在日常编写 C++ 代码时,std::string 是我们处理文本的首选工具。不过在调用 C 语言标准库函数、操作系统接口或部分第三方库时,很多函数只接收以 '\0' 结尾的 const char* 指针或 char 数组,也就是前面学过的 C 风格字符串。
遇到这类场景时,我们就需要在 std::string 和字符数组之间互相转换。把 std::string 往外转换时,如果只是传给函数读取内容,获取只读指针就可以了。如果还需要修改里面的字符,则要把内容复制到一个独立的可写数组中。
string 转 const char* 只读指针
在 C++ 中,std::string 内部会自动管理一块连续的字符内存。如果我们只是想把它作为只读字符串传给接收 const char* 参数的函数,不需要新建字符数组,直接调用成员函数 c_str() 即可。其中,c_str 是英文 “C-style string(C 风格字符串)” 的缩写。
语法:
字符串.c_str()说明:
c_str() 会返回一个 const char* 类型的只读指针,直接指向当前 std::string 内部保存的字符数组,并且保证末尾带有结束符 '\0'。
示例 1:获取只读字符指针
#include <cstring> // 提供 std::strlen()
#include <iostream>
#include <string>
int main() {
std::string path = "config.ini";
const char* ptr = path.c_str(); // 获取只读字符指针
std::cout << ptr << '\n';
std::cout << "字符长度:" << std::strlen(ptr) << '\n';
return 0;
}运行结果如下。
config.ini
字符长度:10分析:
path.c_str() 并没有复制一份新的字符数组,而是直接返回了指向 path 内部字符数据的只读指针 ptr。拿到该指针后,我们既可以直接打印它,也可以把它传给 <cstring> 头文件的 std::strlen() 函数来统计字符长度。
在使用 c_str() 时,有一个初学者极易踩中的坑点:c_str() 返回的指针只是 “借用” 了原 std::string 内部的内存地址。一旦原字符串被销毁,或者因拼接、修改而重新分配了内存,之前拿到的旧指针就会立刻失效,变成危险的悬空指针。
std::string text = "hello";
const char* oldPtr = text.c_str();
text += " C++"; // 拼接后原内存可能已释放
// 正确方式
const char* newPtr = text.c_str();
std::cout << newPtr << '\n';
// 错误方式
std::cout << oldPtr << '\n';如上所示,执行 text += " C++"; 修改字符串之后,千万不要再继续读取旧指针 oldPtr。如果后续仍需使用指针,我们重新调用一次 text.c_str() 获取最新地址即可。
string 转可写 char 数组
通过 c_str() 拿到的只是只读指针,不能用来修改字符。如果我们需要改动数组里的字符,或者某个接口要求传入一块可写的 char 缓冲区,就需要把 std::string 的内容复制出来,常用的有以下 2 种方式。
1. 复制到固定大小的 char 数组
如果字符串的最大长度已知且固定,我们可以先定义一个足够大的 char 数组,再调用 <cstring> 头文件中的 std::strcpy() 函数把内容复制过去。
语法:
std::strcpy(目标字符数组, 字符串.c_str())说明:
std::strcpy() 会从 c_str() 返回的地址开始,把所有字符连同末尾的结束符 '\0' 一起复制到目标字符数组中。
示例 2:复制到固定大小的数组
#include <cstring> // 提供 std::strcpy()
#include <iostream>
#include <string>
int main() {
std::string text = "lvye";
char arr[10];
std::strcpy(arr, text.c_str()); // 复制字符与 '\0'
arr[0] = 'L'; // 修改数组首字符
std::cout << arr << '\n';
std::cout << text << '\n';
return 0;
}运行结果如下。
Lvye
lvye分析:
在这个例子中,"lvye" 包含 4 个可见字符,加上末尾的结束符 '\0' 一共占 5 个字节,因此长度为 10 的 arr 数组完全装得下。复制完成后,arr 拥有独立的内存空间,我们把 arr[0] 修改为 'L' 不会影响原来的 text。
这里小伙伴们一定要算清楚数组长度:目标数组的大小至少要等于 text.size() + 1,多出来的 1 个位置专门用来存放末尾的 '\0'。下面这种只按可见字符数定义数组的写法就是错误的。
std::string text = "lvye";
char arr[4];
std::strcpy(arr, text.c_str()); // 错误:装不下末尾的 '\0'一旦漏算了末尾的 '\0',复制时就会写到数组外面导致内存越界。此外,Visual Studio 编译器默认会因为安全检查对 std::strcpy() 报错,平时更推荐大家使用下面这种动态缓冲区做法。
2. 复制到动态字符缓冲区(推荐)
在实际开发中,字符串的长度往往是在程序运行时才确定的,很难提前写死 char[10] 这样的固定大小。这时我们可以借助 vector 容器,创建一个 std::vector<char> 动态字符缓冲区。
具体写法很简单:text.begin() 和 text.end() 分别代表原字符串的起始和结束位置,我们在构造 vector 时传入它们,就能一步复制全部字符。接着调用 push_back('\0') 补上结束符,再通过 buf.data() 就能拿到指向底层数组的可写 char* 指针。
示例 3:复制到 vector 缓冲区
#include <iostream>
#include <string>
#include <vector>
int main() {
std::string text = "hello";
// 复制全部字符并补上结束符 '\0'
std::vector<char> buf(text.begin(), text.end());
buf.push_back('\0');
buf[0] = 'H';
std::cout << buf.data() << '\n';
std::cout << text << '\n';
return 0;
}运行结果如下。
Hello
hello分析:
buf.data() 返回的是可读可写的 char* 指针,既可以传给 std::cout 打印,也可以传给需要修改字符内容的 C 语言接口。由于 std::vector<char> 会根据字符串长度自动分配和释放内存,我们就不用再担心数组空间不够了。
char 数组转 string
反过来,当我们手里已经有一个 char 数组或 const char* 指针,需要转换为 std::string 时,可以根据数组末尾是否带有 '\0' 结束符来选择写法。
1. 使用 = 直接转换
只要手里的 char 数组或指针是以 '\0' 结尾的标准 C 风格字符串,我们直接使用 = 赋值给 std::string 即可。
语法:
std::string 变量名 = 字符数组或指针;说明:
std::string 会从起始地址开始逐个复制字符,直到遇到第一个结束符 '\0' 为止,并在自己内部开辟独立的内存来保存这些字符。
示例 4:字符数组直接转为 string
#include <iostream>
#include <string>
int main() {
char host[] = "lvyenet";
std::string url = host; // 直接转为 string
url += ".com";
std::cout << host << '\n';
std::cout << url << '\n';
return 0;
}运行结果如下。
lvyenet
lvyenet.com分析:
host 是一个以 '\0' 结尾的字符数组,通过 std::string url = host; 即可自动完成转换。除了在定义变量时直接初始化之外,对已经定义好的 std::string 变量直接写 url = host; 赋值也是等价的。
转换完成后,url 拥有自己独立的内存空间,因此后面执行 url += ".com"; 不会影响原来的 host 数组。
2. 指定长度构造 string
如果我们只想从 char 数组中截取前 n 个字符转为 std::string,或者手里的 char 数组只是一个末尾没有 '\0' 结束符的普通字符序列,可以使用带长度参数的构造函数。
语法:
std::string 变量名(字符数组或指针, 长度);说明:
这种写法不再依赖 '\0' 判断结束,而是直接从起始地址开始复制指定数量的字符,并由 std::string 自动补齐结尾。
示例 5:指定长度转为 string
#include <iostream>
#include <string>
int main() {
char site[] = "lvyenet";
std::string part(site, 4); // 截取前 4 个字符
char raw[3] = {'C', '+', '+'}; // 末尾无 '\0' 的数组
std::string lang(raw, 3); // 指定复制 3 个字符
std::cout << part << '\n';
std::cout << lang << '\n';
return 0;
}运行结果如下。
lvye
C++分析:
std::string part(site, 4); 表示从 site 开头复制前 4 个字符,得到 "lvye"。而 raw 数组只包含 3 个字符且末尾没有 '\0',我们通过 std::string lang(raw, 3); 显式指定复制 3 个字符,就能安全地将其转换为 "C++"。
这里需要特别注意:如果一个 char 数组像上面的 raw 那样末尾没有结束符 '\0',千万不要直接使用 = 赋值给 std::string。如果写成下面这样,程序会越过数组边界一直往后乱读内存,打印出乱码甚至引发崩溃:
char raw[3] = {'C', '+', '+'};
std::string badStr = raw; // 危险:缺少 '\0' 越界读取c_str() 与 data() 的区别
在阅读 C++ 代码或配合 vector 使用时,除了 c_str() 之外,小伙伴们还会见到 data() 成员函数。在 C++20 标准下,对于 std::string 来说,两者都指向内部以 '\0' 结尾的连续字符数组,主要区别在于返回指针的 const 属性:
c_str():无论原 std::string 对象是否为 const,返回类型固定都是 const char* 只读指针。
data():当原对象是非 const 时,返回可写的 char* 指针;当原对象是 const 时,返回 const char* 指针。平时只要是传给只读接口,我们推荐统一使用意图更明确的 c_str()。
