我们编写的程序几乎都会使用字符串。在C语言中,只能使用普通的以null结尾的字符数组来表示字符串。这种表示会有很多问题,例如缓冲区溢出等。然而,C++ STL(standard template library标准模板库)包含了一个安全易用的std::string类,这个类就没有这些缺点。
1. C风格的字符串
在C语言中,字符串就表示为字符的数组。字符串中的最后一个字符是空字符('\0'),只有这样,操作字符串的代码才知道字符串在哪结束。官方将这个空字符定义为NUL,注意,NUL与NULL指针不是一回事,还有nullptr。尽管C++里面有更好的字符串抽象,但是如今这个各种语言遍地跑的年代,没准哪天就用到了C风格的数组。
使用C风格的字符串,最重要的就是要为'\0'分配空间(在我们眼里,空 表示没用,但是在计算机世界里,这个'\0'也要用一串二进制码表示,并不是不存在)。例如,字符串"Hello"看上去是五个字符,但是在内存中需要六个字符的空间才能保存这个字符串的值。
C++中有一些来自C语言的字符串操作函数,它们在<cstring>(不是<string>)头文件中定义。通常这些函数不直接操作内存分配。例如strcpy()函数有两个字符串参数。这个函数将第二个字符串复制到第一个字符串,而不考虑第一个字符串是否装得下第二个字符串。来看下面这段代码:
char* CopyString(const char* str)
{
char* result = new char[strlen(str)];
strcpy(result,str); //这里有bug,跟上句话有关系。
return result;
}
这里strlen(str)返回的是不包含末尾的'\0'的字符串的长度,对于"Hello"来说,strlen("Hello")返回的值是5,而不是6。但是实际上,result需要六个存储char类型字符的空间,上段代码只给了result五个这样的空间。
所以正确的代码应该是:
char* CopyString(const char* str)
{
char* result = new char[strlen(str)+1];
strcpy(result,str);
return result;
}
要思考的问题是:如果要将三个字符串合并成一个字符串时怎么做?依旧是三个字符串的strlen()操作之后加一,这也是为什么strlen只返回实际字符的数目,而不是实际所占空间的大小。
strcat()函数可以将字符串串联:char* appendStrings(const char* str1,const char* str2,const char* str3)
{
char* result=new char[strlen(str1)+strlen(str2)+strlen(str3)+1];
strcpy(result,str1);
strcat(result,str2);
strcat(result,str3);
return result;
}
既然说了strlen(),把sizeof()也一并说了。C和C++中的sizeof()操作符可用于获得给定数据类型或变量的大小。例如,sizeof(char)为1,因为char的大小是1字节。但是,在C中,sizeof()和strlen()是不同的。绝对不要通过sizeof()获得字符串的大小。(这里提一下C风格的字符串的两种表示方法)。
char text1[]="abcdef";
size_t s1=sizeof(text); //返回值为7
size_t s2=strlen(text); //返回值为6
但是,如果C风格的字符串存储为char*,sizeof()就返回指针的大小。
const char* text1="abcdef";
size_t s3=sizeof(text1); // 32为系统返回的是4,64位系统返回的是8
size_t s4=strlen(text1); //返回值为6
可以在<cstring>头文件中找到操作字符串的C函数的完整列表。
2. 字符串字面量
C++程序中的字符串要用 "" 包围。
cout<<"Hello"<<endl;
这段代码输出字符串hello。这段代码包含这个字符串本身而不是包含这个字符串的变量,就是说不是这样的
char str[]="Hello";
cout<<str<<endl;
在cout<<"Hello"<<endl;中,"Hello"是一个字符串字面量,因为这个字符串以值的形式写出,而不是一个变量。与字符串字面量关联的真正内存 在内存的只读部分中。通过这种方式,编译器可以重用等价字符串字面量的引用,来优化内存的使用。换个说法就是,你的程序里有100个cout<<"Hello"<<endl;这种语句,也就是使用了100次"Hello"字符串字面量,编译器也只在内存中创建了1个Hello实例,而不是100个。这种技术称为字面量池(literal pooling)。
字符串字面量可以赋值给变量,但因为字符串字面量位于内存的只读部分,并且使用了字面量池,所以这么做是有一定风险的(想想字符串字面量赋值给一个变量之后,又去修改这个变量)。C++标准正式指出:字符串字面量的类型为"n个const char 数组"。但是,为了向后兼容老版本的不支持const 的代码(版本兼容问题烦得很),大部分编译器不会强制程序将字符串字面量赋值给const char *类型的变量。这些编译器允许将字符串赋值给不带有const 的char*,而且整个程序可以正常运行,除非试图修改字符串。一般情况下,试图修改字符串的行为是没有定义的。这种行为可能会导致很多种结果,可能没什么影响(最好是这样,不然你根本想不到bug在哪),也有可能程序崩溃,最可怕的是行为的不同取决于编译器(编译器也是人写的,学过编译原理就会写了)。看代码:
char* ptr="Hello"; //将一个字符串字面量赋值给变量ptr
ptr[1]='a'; //这是一个没有定义的行为,一般情况下这个应该是没错的。
一种更安全的编码方法是在引用字符串字面量时,使用指向const字符的指针。下面的代码包含同样的bug,但是由于这段代码将字符串字面量赋值给const char*,所以编译器会捕捉到任何写入只读内存的企图,然后,错。
const char* ptr = "Hello";
ptr[1]='a';//报错
还可以将字符串字面量用作字符数组的初始值。这种情况下,编译器会创建一个能够装得下这个字符串的数组,然后将字符串复制到这个数组。因此,编译器不会将字面量放在只读的内存中,也不会进行字面量的池操作。
char str[]="Hello";
arr[1]='a';