C++ string 与 char 数组互转

在日常编写 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()。

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号