尽管 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++"; // null terminator added automatically
const char *cp = "C++"; // null terminator added automatically
char *cp1 = ca1; // points to first element of a array, but not C-style string
char *cp2 = ca2; // points to first element of 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;
}
如果 cp 所指向的字符数组没有 null 结束符,则此循环将会失败。这时,循环会从 cp 指向的位置开始读数,直到遇到内存中某处 null 结束符为止。
C 风格字符串的标准库函数要使用这些标准库函数,必须包含相应的 C 头文件,cstring 是 string.h 头文件的 C++ 版本,而 string.h 则是 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 |
传递给这些标准库函数例程的指针必须具有非零值,并且指向以 null 结束的字符数组中的第一个元素。
其中一些标准库函数会修改传递给它的字符串,这些函数将假定它们所修改的字符串具有足够大的空间接收本函数新生成的字符,程序员必须确保目标字符串必须足够大。
比较字符串
C++ 语言提供普通的关系操作符实现标准库类型 string 的对象的比较。这些操作符也可用于比较指向 C 风格字符串的指针,但效果却很不相同:实际上,此时比较的是指针上存放的地址值,而并非它们所指向的字符串:
if (cp1 < cp2) // compares addresses, not the values pointed o
如果 cp1 和 cp2 指向同一数组中的元素(或该数组的溢出位置),上述表达式等效于比较在 cp1 和 cp2 中存放的地址;如果这两个指针指向不同的数组,则该表达式实现的比较没有定义。
字符串的比较和比较结果的解释都须使用标准库函数 strcmp 进行:
const char *cp1 = "A string example";
const char *cp2 = "A different string";
int i = strcmp(cp1, cp2); // i is positive
i = strcmp(cp2, cp1); // i is negative
i = strcmp(cp1, cp1); // i is zero
永远不要忘记字符串结束符 null
在使用处理 C 风格字符串的标准库函数时,牢记字符串必须以结束符 null结束:
char ca[] = {'C', '+', '+'}; // not null-terminated
cout << strlen(ca) << endl; // disaster: ca isn't null-terminated
ca 是一个没有 null 结束符的字符数组,则计算的结果不可预料。标准库函数 strlen 总是假定其参数字符串以 null 字符结束,当调用该标准库函数时,系统将会从实参 ca 指向的内存空间开始一直搜索结束符,直到恰好遇到 null 为止。strlen 返回这一段内存空间中总共有多少个字符,无论如何这个数值不可能是正确的。
调用者必须确保目标字符串具有足够的大小
传递给标准库函数 strcat 和 strcpy 的第一个实参数组必须具有足够大的空间存放新生成的字符串。
下面的代码很常见,但是极易发生严重问题:
strcpy(largestr,cal);
strcat(largestr, " ")
strcat(largestr,ca2);
一个潜在的问题就是,估算largestr所需的空间时不容易估准,并且,一旦largestr所存的内容改变,就必须重新检查其空间是否足够。
使用 strn 函数处理 C 风格字符串
如果必须使用 C 风格字符串,则使用标准库函数 strncat 和 strncpy 比strcat 和 strcpy 函数更安全:
char largeStr[16 + 18 + 2]; // to hold cp1 a space and cp2
strncpy(largeStr, cp1, 17); // size to copy includes the null
strncat(largeStr, " ", 2); // pedantic, but a good habit
strncat(largeStr, cp2, 19); // adds at most 18 characters, plus a null
使用标准库函数 strncat 和 strncpy 的诀窍在于可以适当地控制复制字符的个数。特别是在复制和串连字符串时,一定要时刻记住算上结束符 null。在定义字符串时要切记预留存放 null 字符的空间,因为每次调用标准库函数后都必须以此结束字符串 largeStr。
尽可能使用标准库类型 string
如果使用 C++ 标准库类型 string,则不存在上述问题:
string largeStr = cp1; // initialize large Str as a copy of cp1
largeStr += " "; // add space at end of largeStr
largeStr += cp2; // concatenate cp2 onto end of largeStr
此时,标准库负责处理所有的内存管理问题,我们不必再担心每一次修改字符串时涉及到的大小问题。
对大部分的应用而言,使用标准库类型 string,除了增强安全性外,效率也提高了,因此应该尽量避免使用 C 风格字符串。
新旧代码的兼容混合使用标准库类 string 和 C 风格字符串
由于 C 风格字符串与字符串字面值具有相同的数据类型,而且都是以空字符 null 结束,因此可以把 C 风格字符串用在任何可以使用字符串字面值的地方:
可以使用 C 风格字符串对 string 对象进行初始化或赋值。
string 类型的加法操作需要两个操作数, 可以使用 C 风格字符串作为其中的一个操作数, 也允许将 C 风格字符串用作复合赋值操作的右操作数。
反之则不成立:在要求 C 风格字符串的地方不可直接使用标准库 string 类型对象。例如,无法使用 string 对象初始化字符指针:
char *str = st2; // compile-time type error
但是,string 类提供了一个名为 c_str 的成员函数,以实现我们的要求:
char *str = st2.c_str(); // almost ok, but not quite
c_str 函数返回 C 风格字符串,其字面意思是:“返回 C 风格字符串的表示方法”,即返回指向字符数组首地址的指针,该数组存放了与 string 对象相同的内容,并且以结束符 null 结束。
如果 c_str 返回的指针指向 const char 类型的数组,则上述初始化失败,这样做是为了避免修改该数组。正确的初始化应为:
const char *str = st2.c_str(); // ok
c_str 返回的数组并不保证一定是有效的,接下来对 str的操作有可能会改变 st2 的值,使刚才返回的数组失效。如果程序需要持续访问该数据,则应该复制 c_str 函数返回的数组。
使用数组初始化 vector 对象
不能用一个数组直接初始化另一数组,只能创建新数组,然后显式地把源数组的元素逐个复制给新数组。
可以使用数组初始化 vector 对象,须指出用于初始化式的第一个元素以及数组最后一个元素的下一位置的地址:
const size_t arr_size = 6;
int int_arr[arr_size] = {0, 1, 2, 3, 4, 5};
// ivec has 6 elements: each a copy of the corresponding element in int_arr
vector<int> ivec(int_arr, int_arr + arr_size);
传递给 ivec 的两个指针标出了 vector 初值的范围。第二个指针指向被复制的最后一个元素之后的地址空间。被标出的元素范围可以是数组的子集:
// copies 3 elements: int_arr[1], int_arr[2], int_arr[3]
vector<int> ivec(int_arr + 1, int_arr + 4);
这个初始化创建了含有三个元素的 ivec,三个元素的值分别是 int_arr[1] 到int_arr[3] 的副本。