4.3 C 风格字符串
尽管 C++ 支持 C 风格字符串,但不应该在 C++ 程序中使用这个类型。C 风格字符串常常带来许多错误,是导致大量安全问题的根源。
在前面我们第一次使用了字符串字面值,并了解字符串字面值的类型是字符常量的数组,现在可以更明确地认识到:字符串字面值的类型就是const char 类型的数组。C++ 从 C 语言继承下来的一种通用结构是C 风格字符串,而字符串字面值就是该类型的实例。实际上,C 风格字符串既不能确切地归结为 C 语言的类型,也不能归结为 C++ 语言的类型,而是以空字符 null 结束的字符数组:
char ca1[] = {'C', '+', '+'}; // no null,not C-style
string
char ca2[] = {'C', '+', '+', '\0'}; //explicit null
char ca3[] = "C++"; // nullterminator added automatically
const char *cp = "C++"; // nullterminator added automatically
char *cp1 = ca1; // points to first elementof a array, but not C-style string
char *cp2 = ca2; // points to first elementof a null-terminated char array
ca1 和 cp1 都不是 C 风格字符串:ca1 是一个不带结束符 null 的字符数组,而指针 cp1 指向 ca1,因此,它指向的并不是以 null 结束的数组。其他的声明则都是 C 风格字符串,数组的名字即是指向该数组第一个元素的指针。于是,ca2 和 ca3 分别是指向各自数组第一个元素的指针。
C 风格字符串的使用
C++ 语言通过(const)char*类型的指针来操纵 C 风格字符串。一般来说,我们使用指针的算术操作来遍历 C 风格字符串,每次对指针进行测试并递增 1,直到到达结束符 null 为止:
const char *cp = "some value";
while (*cp)
{
// do something to *cp
++cp;
}
while 语句的循环条件是对const char* 类型的指针 cp 进行解引用,并判断cp 当前指向的字符是 true 值还是 false 值。真值表明这是除 null 外的任意字符,则继续循环直到 cp 指向结束字符数组的 null 时,循环结束。
while 循环体做完必要的处理后,cp加1,向下移动指针指向数组中的下一个字符。
如果 cp 所指向的字符数组没有null 结束符,则此循环将会失败。这时,循环会从 cp 指向的位置开始读数,直到遇到内存中某处 null 结束符为止。
C 风格字符串的标准库函数
表列出了 C 语言标准库提供的一系列处理 C 风格字符串的库函数。要
使用这些标准库函数,必须包含相应的 C 头文件:
cstring 是 string.h 头文件的 C++ 版本,而 string.h 则是 C 语言提供
的标准库。
这些标准库函数不会检查其字符串参数。
表 4.1. 操纵 C 风格字符串的标准库函数
strlen(s) 返回 s 的长度,不包括字符串结束符 null
strcmp(s1, s2) 比较两个字符串 s1 和 s2 是否相同。若 s1 与 s2 相等,返
回 0;若 s1 大于 s2,返回正数;若 s1 小于 s2,则返回负
数
strcat(s1, s2) 将字符串s2 连接到 s1 后,并返回 s1
strcpy(s1, s2) 将 s2复制给 s1,并返回 s1
strncat(s1,s2,n) 将s2 的前 n 个字符连接到 s1 后面,并返回 s1
strncpy(s1,s2, n) 将s2 的前 n 个字符复制给 s1,并返回 s1
#include <cstring>
传递给这些标准库函数例程的指针必须具有非零值,并且指向以 null 结束的字符数组中的第一个元素。其中一些标准库函数会修改传递给它的字符串,这些函数将假定它们所修改的字符串具有足够大的空间接收本函数新生成的字符,程序员必须确保目标字符串必须足够大。
C++ 语言提供普通的关系操作符实现标准库类型 string 的对象的比较。这些操作符也可用于比较指向C 风格字符串的指针,但效果却很不相同:实际上,此时比较的是指针上存放的地址值,而并非它们所指向的字符串:
if (cp1 < cp2) // compares addresses,not the values pointed to
如果 cp1 和 cp2 指向同一数组中的元素(或该数组的溢出位置),上述表
达式等效于比较在 cp1 和 cp2 中存放的地址;如果这两个指针指向不同的数
组,则该表达式实现的比较没有定义。
字符串的比较和比较结果的解释都须使用标准库函数 strcmp 进行:
const char *cp1 = "A stringexample";
const char *cp2 = "A differentstring";
int i = strcmp(cp1, cp2); // i is positive
i = strcmp(cp2, cp1); // i is negative
i = strcmp(cp1, cp1); // i is zero
标准库函数 strcmp 有 3 种可能的返回值:若两个字符串相等,则返回 0
值;若第一个字符串大于第二个字符串,则返回正数,否则返回负数。
永远不要忘记字符串结束符 null
在使用处理 C 风格字符串的标准库函数时,牢记字符串必须以结束符 null
结束:
char ca[] = {'C', '+', '+'}; // notnull-terminated
cout << strlen(ca) << endl; //disaster: ca isn't
null-terminated
在这个例题中,ca 是一个没有 null 结束符的字符数组,则计算的结果不
可预料。标准库函数 strlen 总是假定其参数字符串以 null 字符结束,当调用
该标准库函数时,系统将会从实参 ca 指向的内存空间开始一直搜索结束符,直
到恰好遇到 null 为止。strlen 返回这一段内存空间中总共有多少个字符,无
论如何这个数值不可能是正确的。
调用者必须确保目标字符串具有足够的大小
传递给标准库函数 strcat 和strcpy 的第一个实参数组必须具有足够大
的空间存放新生成的字符串。以下代码虽然演示了一种通常的用法,但是却有潜
在的严重错误:
// Dangerous: What happens if wemiscalculate the size of largeStr?
char largeStr[16 + 18 + 2]; // will holdcp1 a space and cp2
strcpy(largeStr, cp1); // copies cp1 intolargeStr
strcat(largeStr, " "); // adds aspace at end of largeStr
strcat(largeStr, cp2); // concatenates cp2to largeStr
// prints A string example A differentstring
cout << largeStr << endl;
问题在于我们经常会算错 largeStr 需要的大小。同样地,如果 cp1 或 cp2所指向的字符串大小发生了变化,largeStr 所需要的大小则会计算错误。不幸的是,类似于上述代码的程序应用非常广泛,这类程序往往容易出错,并导致严重的安全漏洞。
使用strn 函数处理 C 风格字符串
如果必须使用 C 风格字符串,则使用标准库函数 strncat 和 strncpy 比
strcat和 strcpy 函数更安全:
char largeStr[16 + 18 + 2]; // to hold cp1a space and cp2
strncpy(largeStr, cp1, 17); // size to copyincludes the null
strncat(largeStr, " ", 2); //pedantic, but a good habit
strncat(largeStr, cp2, 19); // adds at most18 characters, plus a null
使用标准库函数 strncat 和strncpy 的诀窍在于可以适当地控制复制字符的个数。特别是在复制和串连字符串时,一定要时刻记住算上结束符null。在定义字符串时要切记预留存放 null 字符的空间,因为每次调用标准库函数后都必须以此结束字符串 largeStr。让我们详细分析一下这些标准库函数的调用:
• 调用 strncpy 时,要求复制17 个字符:字符串 cp1 中所有字符,加上结束符 null。留下存储结束符 null 的空间是必要的,这样 largeStr 才可以正确地结束。调用 strncpy 后,字符串 largeStr 的长度 strlen 值是 16。
记住:标准库函数 strlen 用于计算 C 风格字符串中的字符个数,不包括 null 结束符。
• 调用 strncat 时,要求复制2 个字符:一个空格和结束该字符串字面值的 null。调用结束后,字符串 largeStr 的长度是 17,原来用于结束largeStr 的 null 被新添加的空格覆盖了,然后在空格后面写入新的结束符 null。
• 第二次调用 strncat 串接cp2 时,要求复制 cp2 中所有字符,包括字符串结束符null。调用结束后,字符串 largeStr 的长度是 35:cp1 的16 个字符和 cp2 的 18 个字符,再加上分隔这两个字符串的一个空格。
整个过程中,存储 largeStr 的数组大小始终保持为 36(包括结束符)。只要可以正确计算出 size 实参的值,使用 strn 版本要比没有 size 参数的简化版本更安全。但是,如果要向目标数组复制或串接比其 size 更多的字符,数组溢出的现象仍然会发生。如果要复制或串接的字符串比实际要复制或串接的size 大,我们会不经意地把新生成的字符串截短了。截短字符串比数组溢出要安全,但这仍是错误的。
尽可能使用标准库类型string
如果使用 C++ 标准库类型string,则不存在上述问题:
string largeStr = cp1; // initialize largeStr as a copy of cp1
largeStr += " "; // add space atend of largeStr
largeStr += cp2; // concatenate cp2 ontoend of largeStr
此时,标准库负责处理所有的内存管理问题,我们不必再担心每一次修改字符串时涉及到的大小问题。对大部分的应用而言,使用标准库类型 string,除了增强安全性外,效率也提高了,因此应该尽量避免使用 C 风格字符串。