阅读早期 C++ 项目代码或调用 C 语言函数库时,我们经常会看到另一种字符串写法:char str[] = "lvye";。
这种使用 char 字符数组来保存文本的方式,通常称为 “C 风格字符串”。它源自 C 语言的字符串表示方法,C++ 为了兼容 C 语言,也一直保留着这种写法。
C 风格字符串的定义
在 C++ 中,C 风格字符串本质上就是一个普通的数组,数组元素全都是 char 字符,并且末尾必须有一个空字符 '\0' 作为结束标志。
语法:
char 字符串名[] = "字符串内容";说明:
当编译器遇到使用英文双引号括起来的字符串字面量时,不仅会把双引号里的字符依次放进 char 数组,还会在末尾自动补上一个空字符 '\0'。以 char str[] = "lvye"; 为例,它在内存中的存储结构如下表所示。
| 元素下标 | 保存字符 | 说明 |
|---|---|---|
| 0 | 'l' | 第 1 个可见字符 |
| 1 | 'v' | 第 2 个可见字符 |
| 2 | 'y' | 第 3 个可见字符 |
| 3 | 'e' | 第 4 个可见字符 |
| 4 | '\0' | 编译器自动补上的结束标志 |
示例 1:定义 C 风格字符串
#include <iostream>
int main() {
char message[] = "Hello, C++!";
std::cout << message << '\n';
return 0;
}运行结果如下。
Hello, C++!分析:
char message[] = "Hello, C++!"; 表示定义了一个字符数组,双引号里的字符会依次存入数组,末尾还会自动补上 '\0'。当 std::cout 输出 message 时,会从首字符开始往后打印,直到遇到 '\0' 停止。
由于 C 风格字符串本质上还是数组,我们同样可以通过下标来访问或修改里面的单个字符,比如 message[0] 就代表首字符 'H'。如果要定义一个保存 "lvye" 的 C 风格字符串,下面 2 种方式是等价的:
// 方式 1(推荐)
char str[] = "lvye";
// 方式 2
char str[] = {'l', 'v', 'y', 'e', '\0'};其中,方式 1 使用双引号字面量,编译器会自动在末尾补上 '\0'。而方式 2 使用花括号逐个列出字符,必须由我们手动写上 '\0'。在实际开发中,我们一般都使用方式 1,既简单直观,又不会漏写结尾的结束符。
字符串结束标志 \0
C 风格字符串最关键的地方,就是末尾那个空字符 '\0'。因为 char 数组本身不会单独记录文本有多长,C++ 在处理 C 风格字符串时,全靠寻找 '\0' 来判断字符串在哪里结束。
这里小伙伴们可别把空字符 '\0' 和数字字符 '0' 搞混了。'\0' 是一种转义字符,它在底层的编码值为 0,不会在屏幕上显示任何内容。而 '0' 是平时看到的数字字符 “0”,对应的 ASCII 码为 48,两者不是一回事。
1. 区分 sizeof 与 std::strlen()
如果想知道一个 C 风格字符串里实际包含多少个有效字符,我们可以使用 <cstring> 头文件中的 std::strlen() 函数,并结合 sizeof 运算符来验证末尾隐藏的 '\0'。
语法:
#include <cstring>
std::strlen(字符数组)说明:
其中,strlen 是 “string length(字符串长度)” 的缩写。std::strlen() 会从字符数组的开头逐个往后统计,直到遇到 '\0' 为止,返回 '\0' 之前的有效字符个数,不包含 '\0' 本身。
示例 2:有效长度与数组大小
#include <cstring>
#include <iostream>
int main() {
char word[] = "lvye";
std::cout << "有效字符数:" << std::strlen(word) << '\n';
std::cout << "数组总字节数:" << sizeof(word) << '\n';
std::cout << "下标 4 是否为空字符:" << (word[4] == '\0') << '\n';
return 0;
}运行结果如下。
有效字符数:4
数组总字节数:5
下标 4 是否为空字符:1分析:
从运行结果可以看出,"lvye" 中可见的英文字母有 4 个,所以 std::strlen(word) 的结果是 4。而 sizeof(word) 统计的是整个数组占用的总字节数,会把末尾隐藏的 '\0' 也算进去,因此结果是 5。
同时,第三行 word[4] == '\0' 的比较结果为 true,控制台打印为 1,这也证实了下标 4 的位置保存的正是空字符 '\0'。
因此,我们在定义 C 风格字符串时如果手动指定了数组长度,就必须把末尾 '\0' 的位置也预留出来。也就是说,数组长度至少要比可见字符数大 1 才行:
// 正确方式
char a[5] = "lvye";
char b[10] = "lvye";
// 错误方式
char c[4] = "lvye";上面代码中,"lvye" 加上末尾的 '\0' 一共需要 5 个位置,写成 char c[4] 放不下结束符,编译时就会直接报错。如果没有固定的容量需求,直接写成 char a[] = "lvye"; 让编译器自动计算长度会更加省心。
2. 遇到 \0 停止处理
接下来我们再看一个例子,直观感受一下 “遇到 '\0' 就认为字符串结束” 的规则。
示例 3:遇到 \0 停止输出
#include <iostream>
int main() {
char text[] = {'l', 'v', 'y', 'e', '\0', 'X', 'Y'};
std::cout << text << '\n';
std::cout << text[5] << text[6] << '\n';
return 0;
}运行结果如下。
lvye
XY分析:
在这个例子中,text 数组存了 7 个字符,通过 text[5] 和 text[6] 依然能取出 'X' 和 'Y'。但当 std::cout 把 text 当作字符串输出时,遇到下标 4 的 '\0' 就会立即停止,因此第一行只输出了 lvye。
反过来,并不是所有的 char 数组都是 C 风格字符串。比如 char letters[] = {'O', 'K'}; 末尾没有写上 '\0',它就只是一个普通的字符数组,而不是 C 风格字符串。
注意 没有 '\0' 结尾的普通 char 数组,千万不要直接传给 std::cout 或 std::strlen()。否则程序找不到停止标志,会继续往数组外读取内存,导致打印乱码甚至崩溃。
与 std::string 的对比
C++ 保留 C 风格字符串,主要是为了兼容传统的 C 语言函数库和系统底层接口。在日常编写现代 C++ 程序时,处理文本我们推荐优先选择 std::string。
相比 std::string,C 风格字符串本质上是固定长度的原生数组,日常使用主要有以下 2 个不便之处:
- 容量固定且需要手动管理:定义数组时一旦确定了大小,后续就无法自动扩容,拼接长文本时如果空间算小了很容易引发内存越界。
- 不支持直接整体赋值或拼接:数组创建完成后,不能直接使用等号(=)整体赋一段新文本,也不能直接使用加号(+)把两个字符数组拼在一起,而必须借助
<cstring>中的std::strcpy()、std::strcat()等传统函数。
比如像下面这样直接对已定义好的字符数组重新赋一段文本,编译时就会直接报错:
char site[] = "lvye";
site = "lvyenet";在实际开发中,平时处理字符串统一使用 std::string 即可。如果遇到传统函数要求传入 C 风格字符串,可以调用 c_str() 函数进行转换,后面在 “C++ string 与 char 数组互转” 一节中会详细介绍。
