C 风格字符串

尽管C++支持C风格字符串,但在C++程序中最好还是不要使用它们。这是因为C风格字符串不仅使用起来不太方便,而且极易引发程序漏洞,是诸多安全问题的根本原因。

字符串字面值是一种通用结构的实例,这种结构即是C++由C继承而来的C风格字符串(C-style character string)。C风格字符串不是一种类型,而是为了表达和使用字符串而形成的一种约定俗成的写法。按此习惯书写的字符串存放在字符数组中并以空字符结束(null terminated)。以空字符结束的意思是在字符串最后一个字符后面跟着一个空字符(’\0’)。一般利用指针来操作这些字符串。

1. C标准库String函数

表3.8列举了C语言标准库提供的一组函数,这些函数可用于操作C风格字符串,它们定义在cstring头文件中,cstring是C语言头文件string.h的C++版本。

这里写图片描述

表3.8所列的函数不负责验证其字符串参数。

传入此类函数的指针必须指向以空字符作为结束的数组:

char ca[] = {'C', '+', '+'};    // 不以空字符结束  
cout << strlen(ca) << endl;     // 严重错误:ca没有以空字符结束 

此例中,ca虽然也是一个字符数组但它不是以空字符作为结束的,因此上述程序将产生未定义的结果。strlen函数将有可能沿着ca在内存中的位置不断向前寻找,直到遇到空字符才停下来。

2. 比较字符串
比较两个C风格字符串的方法和之前学习过的比较标准库string对象的方法大相径庭。比较标准库string对象的时候,用的是普通的关系运算符和相等性运算符:

string s1 = "A string example";  
string s2 = "A different string";  
if (s1 < s2) // false:s2小于s1  

如果把这些运算符用在两个C风格字符串上,实际比较的将是指针而非字符串本身:

const char ca1[] = "A string example";  
const char ca2[] = "A different string";  
if (ca1 < ca2)  // 未定义的:试图比较两个无关地址 

当使用数组的时候其实真正用的是指向数组首元素的指针。因此,上面的if条件实际上比较的是两个const char*的值。这两个指针指向的并非同一对象,所以将得到未定义的结果。

要想比较两个C风格字符串需要调用strcmp函数,此时比较的就不再是指针了。如果两个字符串相等,strcmp返回0;如果前面的字符串较大,返回正值;如果后面的字符串较大,返回负值:

if (strcmp(ca1, ca2) < 0) // 和两个string对象的比较 s1 < s2效果一样

3. 目标字符串的大小由调用者指定

连接或拷贝C风格字符串也与标准库string对象的同类操作差别很大。例如,要想把刚刚定义的那两个string对象s1和s2连接起来,可以直接写成下面的形式:

// 将largeStr初始化成s1、一个空格和s2的连接  
string largeStr = s1 + " " + s2; 

同样的操作如果放到ca1和ca2这两个数组身上就会产生错误了。表达式ca1 + ca2试图将两个指针相加,显然这样的操作没什么意义,也肯定是非法的。

正确的方法是使用strcat函数和strcpy函数。不过要想使用这两个函数,还必须提供一个用于存放结果字符串的数组,该数组必须足够大以便容纳下结果字符串及末尾的空字符。下面的代码虽然很常见,但是充满了安全风险,极易引发严重错误:

// 如果我们计算错了largeStr的大小将引发严重错误  
strcpy(largeStr, ca1);      // 把ca1拷贝给largeStr  
strcat(largeStr, " ");      // 在largeStr的末尾加上一个空格  
strcat(largeStr, ca2);      // 把ca2连接到largeStr后面  

一个潜在的问题是,我们在估算largeStr所需的空间时不容易估准,而且largeStr所存的内容一旦改变,就必须重新检查其空间是否足够。不幸的是,这样的代码到处都是,程序员根本没法照顾周全。这类代码充满了风险而且经常导致严重的安全泄漏。

对大多数应用来说,使用标准库string要比使用C风格字符串更安全、更高效。

与旧代码的接口
4. 混用string对象和C 风格字符串
任何出现字符串字面值的地方都可以用以空字符结束的字符数组来替代:

  • 允许使用以空字符结束的字符数组来初始化string对象或为string对象赋值。
  • 在string对象的加法运算中允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个运算对象都是);在string对象的复合赋值运算中允许使用以空字符结束的字符数组作为右侧的运算对象。

上述性质反过来就不成立了:如果程序的某处需要一个C风格字符串,无法直接用string对象来代替它。例如,不能用string对象直接初始化指向字符的指针。为了完成该功能,string专门提供了一个名为c_str的成员函数:

char *sstr = s; // 错误:不能用string对象初始化char*  
const char *sstr = s.c_str(); // 正确  

c_str函数的返回值是一个C风格的字符串。也就是说,函数的返回结果是一个指针,该指针指向一个以空字符结束的字符数组,而这个数组所存的数据恰好与那个string对象的一样。结果指针的类型是const char*,从而确保我们不会改变字符数组的内容。

我们无法保证c_str函数返回的数组一直有效,事实上,如果后续的操作改变了s的值就可能让之前返回的数组失去效用。

如果执行完c_str()函数后程序想一直都能使用其返回的数组,最好将该数组重新拷贝一份。

5. 使用数组初始化vector对象

不允许使用一个数组为另一个内置类型的数组赋初值,也不允许使用vector对象初始化数组。相反的,允许使用数组来初始化vector对象。要实现这一目的,只需指明要拷贝区域的首元素地址和尾后地址就可以了:

int int_arr[] = {0, 1, 2, 3, 4, 5};  
// ivec有6个元素,分别是int_arr中对应元素的副本  
vector<int> ivec(begin(int_arr), end(int_arr))

在上述代码中,用于创建ivec的两个指针实际上指明了用来初始化的值在数组int_arr中的位置,其中第二个指针应指向待拷贝区域尾元素的下一位置。此例中,使用标准库函数begin和end来分别计算int_arr的首指针和尾后指针。在最终的结果中,ivec将包含6个元素,它们的次序和值都与数组int_arr完全一样。

用于初始化vector对象的值也可能仅是数组的一部分:

// 拷贝三个元素:int_arr[1]、int_arr[2]、int_arr[3]  
vector<int> subVec(int_arr + 1, int_arr + 4);  

这条初始化语句用3个元素创建了对象subVec,3个元素的值分别来自int_arr[1]、int_arr[2]和int_arr[3]。

建议:尽量使用标准库类型而非数组

使用指针和数组很容易出错。一部分原因是概念上的问题:指针常用于底层操作,因此容易引发一些与烦琐细节有关的错误。其他问题则源于语法错误,特别是声明指针时的语法错误。

现代的C++程序应当尽量使用vector和迭代器,避免使用内置数组和指针;应该尽量使用string,避免使用C风格的基于数组的字符串。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值