11.6 - C 语言风格字符串
Key Takeaway
- C语言字符串就是一个字符数组,最后接着一个空字符。
- 打印字符串时,遇到空字符会停止,如果空字符被覆盖,打印函数就会一直打印直到遇到下一个0
- 调用
std::cin.getline(name, std::size(name));
可以读取最多 254 个字符到name
(为空字符保留一个空间!)。超出的字符会被忽略掉。通过这个方法,可以避免数组溢出! - 操作C语言字符串的常用函数:
strcpy()
-- 将字符串拷贝到另一个字符串strncpy()
-- 将字符串拷贝到另一个字符串(指定buffer大小),但不能确保null结尾strcpy_s()
,它添加了一个新参数来定义目标的大小(C++11)strcat()
-- 将一个字符串拼接到另一个字符串后面(危险操作)strncat()
-- 将一个字符串拼接到另一个字符串后面 (会检查buffer长度)strcmp()
-- 比较两个字符串(相等返回 0)strncmp()
-- 比较两个字符串中特定格式的字符(相等返回 0)
- 不要使用 C 语言风格的字符串
在4.17 - std::string 简介中,我们将字符定义为一系列字符的集合,例如 “Hello, world!”。字符串c++中处理文本的主要方式,而 std::string
可以使C++中处理字符串变得简单。
现代C++支持两种不同类型的字符串:std::string
(作为标准库的一部分)和C语言风格的字符串(从C语言继承而来)。事实证明,std::string
是使用c风格字符串实现的。在这一课中,我们将进一步了解C语言风格的字符串。
C 语言风格字符串
C语言风格字符串就是一个以空字符(\0
)结尾的字符数组。空字符是一个特殊的字符('\0'
, ascii 码为 0) ,用于表示字符的结尾。所以C语言风格字符串也被称为空字符结尾的字符串。
定义C语言风格字符串,可以声明一个字符数组,然后使用字符串字面量对其进行初始化。
1 |
|
尽管 “string” 只有6个字母,C++会自动在其末尾添加一个空字符表明结尾(不需要程序员自己操作)。这样一来,myString
数组的长度实际上为 7!
我们可以在下面的程序中看到这一点的证据,它打印出字符串的长度以及所有字符的ASCII值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
打印结果为:
1 2 |
|
0
是添加到字符串末尾的空结束符的ASCII码。
当以这种方式声明字符串时,最好使用[]
,并让编译器计算数组的长度。这样,如果以后需要修改字符串,就不必手动调整数组的长度。
需要注意的一点是,C风格的字符串遵循与数组相同的规则。这意味着你可以在创建时初始化字符串,但你不能在之后使用赋值操作符给它赋值!
1 2 |
|
因为 C 风格字符串是数组,所以你可以使用下标运算符 []
来改变字符串中的元素:
1 2 3 4 5 6 7 8 9 10 |
|
程序打印:
1 |
|
在使用 C 风格字符串时,std::cout
会依次打印空字符。如果你不小心覆盖了字符串中的空字符(例如:给myString[6]
赋值),那么你得到的打印结果不仅仅是完整的字符串,std::cout
还会继续打印相邻内存中的内容,直到遇到 0。
注意,字符数组的大小比字符串大是可以的:
1 2 3 4 5 6 7 8 9 |
|
在这个例子中,“Alex” 会被打印出来,然后 std::cout
会在遇到空字符时停止打印。数组中后续内容会被忽略。
C 语言风格字符串和 std::cin
在很多情况下我们都不知道字符串会有多长。例如,考虑这样一个问题:编写一个程序,我们需要要求用户输入他们的名字。他们的名字有多长?在用户输入被读取之前,我们都不知道!
在本例中,我们声明一个比实际需要大的多的数组:
1 2 3 4 5 6 7 8 9 10 11 |
|
在上面的程序中,我们为name分配了一个255个字符的数组并假设用户不会输入这么多字符。尽管这在C/C++编程中很常见,但这是一种糟糕的编程实践,因为并没有办法阻止用户输入超过254个字符(不管是无意的,还是恶意的)。
使用 std::cin
读取C风格字符串的推荐方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
调用 std::cin.getline(name, std::size(name));
可以读取最多 254 个字符到 name
(为空字符保留一个空间!)。超出的字符会被忽略掉。通过这个方法,可以避免数组溢出!
操作 C 语言风格字符串
C++提供了许多函数来操作C风格的字符串,它们是 <cstring>
头文件的一部分。常用的函数列举如下:
strcpy()
可以将一个字符串复制到另一个字符串。更常见的是,它被用来给一个字符串赋值:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
不过,如果不小心的话,strcpy()
很容易导致数组的溢出。在下面的例子中,目标地址的大小不足以容纳全部字符串,这就会导致数组溢出。
1 2 3 4 5 6 7 8 9 10 11 12 |
|
许多程序员建议使用 strncpy()
,使用它可以指定缓冲区的大小,并确保不会发生溢出。不幸的是,' strncpy()
不能确保字符串以空字符结尾,这仍然为数组溢出留下了隐患。
在C++ 11中,首选 strcpy_s()
,它添加了一个新参数来定义目标的大小。然而,并不是所有的编译器都支持这个函数,要使用它,你必须定义 STDC_WANT_LIB_EXT1
并将其定义为 1 。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
因为并不是所有的编译器都支持 strcpy_s()
,所以 strlcpy()
是一个很受欢迎的选择——尽管它不是标准的,因此很多编译器都不包含它。它也有自己的一系列问题。简而言之,如果您需要复制C风格的字符串,这里没有普遍推荐的解决方案。
另一个有用的函数是 strlen()
函数,它返回C风格字符串的长度(没有空字符)。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
打印结果如下:
1 2 3 |
|
注意辨析 strlen()
和 std::size()
的区别。 strlen()
会返回空字符前所有字符的个数,而 std::size
(或者sizeof()
技巧)则会返回整个数组的大小,不管数组内部存放了什么。
其他函数:
strcat()
-- 将一个字符串拼接到另一个字符串后面(危险操作)
strncat()
-- 将一个字符串拼接到另一个字符串后面 (会检查buffer长度)
strcmp()
-- 比较两个字符串(相等返回 0)
strncmp()
-- 比较两个字符串中特定格式的字符(相等返回 0)
下面是一个使用本课中的一些概念编写的示例程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
注意我们在循环外使用 strlen(buffer)
,这样它之后计算一次,而不是每次循环都去计算。
不要使用 C 语言风格的字符串
了解C语言风格字符串是很重要的,因为它们在很多代码中都有使用。然而,既然已经解释了它的工作原理,建议你尽量避免使用它们!除非有特定的、令人信服的理由使用C语言风格字符串,否则请使用std::string(定义在<string>
头文件中)类型的字符串,它更简单、更安全也更灵活。在极少数情况下,你确实需要使用固定的缓冲区大小和C风格的字符串(例如内存有限的设备),我们建议使用一个测试良好的第三方字符串库,或 std::string_view
,这将在下一课中讨论。
法则
使用 std::string
或 std::string_view
来代替 C 语言风格的字符串。