ISO/ANSI C++98标准通过添加string类扩展了C++库,因此现在可以string类型的变量(使用C++的话说是对象)而不是字符数组来存储字符串。您将看到,string类使用起来比数组简单,同时提供了将字符串作为一种数据类型的表示方法。
要使用string类,必须在程序中包含头文件string。string类位于名称空间std中,因此您必须提供一条using编译指令,或者使用std::string来引用它。string类定义隐藏了字符串的数组性质,让您能够像处理普通变量那样处理字符串。程序清单4.7说明了string对象与字符数组之间的一些相同点和不同点。
程序清单4.7 strtype1.cpp
下面是该程序的运行情况:
从这个示例可知,在很多方面,使用string对象的方式与使用字符数组相同。
程序清单4.7表明,string对象和字符数组之间的主要区别是,可以将string对象声明为简单变量,而不是数组:
类设计让程序能够自动处理string的大小。例如,str1的声明创建一个长度为0的string对象,但程序将输入读取到str1中时,将自动调整str1的长度:
这使得与使用数组相比,使用string对象更方便,也更安全。从理论上说,可以将char数组视为一组用于存储一个字符串的char存储单元,而string类变量是一个表示字符串的实体。
正如您预期的,C++11也允许将列表初始化用于C-风格字符串和string对象:
使用string类时,某些操作比使用数组时更简单。例如,不能将一个数组赋给另一个数组,但可以将一个string对象赋给另一个string对象:
string类简化了字符串合并操作。可以使用运算符+将两个string对象合并起来,还可以使用运算符+=将字符串附加到string对象的末尾。继续前面的代码,您可以这样做:
程序清单4.8演示了这些用法。可以将C-风格字符串或string对象与string对象相加,或将它们附加到string对象的末尾。
程序清单4.8 strtype2.cpp
转义序列\"表示双引号,而不是字符串结尾。该程序的输出如下:
在C++新增string类之前,程序员也需要完成诸如给字符串赋值等工作。对于C-风格字符串,程序员使用C语言库中的函数来完成这些任务。头文件cstring(以前为string.h)提供了这些函数。例如,可以使用函数strcpy( )将字符串复制到字符数组中,使用函数strcat( )将字符串附加到字符数组末尾:
程序清单4.9对用于string对象的技术和用于字符数组的技术进行了比较。
程序清单4.9 strtype3.cpp
下面是该程序的输出:
处理string对象的语法通常比使用C字符串函数简单,尤其是执行较为复杂的操作时。例如,对于下述操作:
使用C-风格字符串时,需要使用的函数如下:
另外,使用字符数组时,总是存在目标数组过小,无法存储指定信息的危险,如下面的示例所示:
函数strcat( )试图将全部12个字符连接到数组site,这将覆盖相邻的内存。这可能导致程序终止,或者程序继续运行,但数据被损坏。string类具有自动调整大小的功能,从而能够避免这种问题发生。C函数库确实提供了与strcat( )和strcpy( )类似的函数—strncat( )和strncpy( ),它们接受指出目标数组最大允许长度的第三个参数,因此更为安全,但使用它们进一步增加了编写程序的复杂度。
下面是两种确定字符串中字符数的方法:
函数strlen( )是一个常规函数,它接受一个C-风格字符串作为参数,并返回该字符串包含的字符数。函数size( )的功能基本上与此相同,但句法不同:str1不是被用作函数参数,而是位于函数名之前,它们之间用句点连接。与第3章介绍的put( )方法相同,这种句法表明,str1是一个对象,而size( )是一个类方法。方法是一个函数,只能通过其所属类的对象进行调用。在这里,str1是一个string对象,而size( )是string类的一个方法。总之,C函数使用参数来指出要使用哪个字符串,而C++ string类对象使用对象名和句点运算符来指出要使用哪个字符串。
正如您知道的,可以使用cin和运算符>>来将输入存储到string对象中,使用cout和运算符<<来显示string对象,其句法与处理C-风格字符串相同。但每次读取一行而不是一个单词时,使用的句法不同,程序清单4.10说明了这一点。
程序清单4.10 strtype4.cpp
下面是一个运行该程序时的输出示例:
在用户输入之前,该程序指出数组charr中的字符串长度为27,这比该数组的长度要大。这里有两点需要说明。首先,未初始化的数组的内容是未定义的;其次,函数strlen( )从数组的第一个元素开始计算字节数,直到遇到空字符。在这个例子中,在数组末尾的几个字节后才遇到空字符。对于未被初始化的数据,第一个空字符的出现位置是随机的,因此您在运行该程序时,得到的数组长度很可能与此不同。
另外,用户输入之前,str中的字符串长度为0。这是因为未被初始化的string对象的长度被自动设置为0。
下面是将一行输入读取到数组中的代码:
这种句点表示法表明,函数getline( )是istream类的一个类方法(还记得吗,cin是一个istream对象)。正如前面指出的,第一个参数是目标数组;第二个参数数组长度,getline( )使用它来避免超越数组的边界。
下面是将一行输入读取到string对象中的代码:
这里没有使用句点表示法,这表明这个getline( )不是类方法。它将cin作为参数,指出到哪里去查找输入。另外,也没有指出字符串长度的参数,因为string对象将根据字符串的长度自动调整自己的大小。
那么,为何一个getline( )是istream的类方法,而另一个不是呢?在引入string前,C++早就有istream类。因此istream的设计考虑到了诸如double和int等基本C++数据类型,但没有考虑string类型,所以istream类中,有处理double、int和其他基本类型的类方法,但没有处理string对象的类方法。
由于istream类中没有处理string对象的类方法,因此您可能会问,下述代码为何可行呢?
像下面这样的代码使用istream类的一个成员函数:
但前面处理string对象的代码使用string类的一个友元函数。有关友元函数及这种技术为何可行,将在第11章介绍。另外,您可以将cin和cout用于string对象,而不用考虑其内部工作原理。
本书前面说过,除char类型外,C++还有类型wchar_t;而C++11新增了类型char16_t和char32_t。可创建这些类型的数组和这些类型的字符串字面值。对于这些类型的字符串字面值,C++分别使用前缀L、u和U表示,下面是一个如何使用这些前缀的例子:
C++11还支持Unicode字符编码方案UTF-8。在这种方案中,根据编码的数字值,字符可能存储为1~4个八位组。C++使用前缀u8来表示这种类型的字符串字面值。
C++11新增的另一种类型是原始(raw)字符串。在原始字符串中,字符表示的就是自己,例如,序列\n不表示换行符,而表示两个常规字符—斜杠和n,因此在屏幕上显示时,将显示这两个字符。另一个例子是,可在字符串中使用",而无需像程序清单4.8中那样使用繁琐的\"。当然,既然可在字符串字面量包含",就不能再使用它来表示字符串的开头和末尾。因此,原始字符串将"(和)"用作定界符,并使用前缀R来标识原始字符串:
上述代码将显示如下内容:
如果使用标准字符串字面值,将需编写如下代码:
在上述代码中,使用了\来显示\,因为单个\表示转义序列的第一个字符。
输入原始字符串时,按回车键不仅会移到下一行,还将在原始字符串中添加回车字符。
如果要在原始字符串中包含)",该如何办呢?编译器见到第一个)"时,会不会认为字符串到此结束?会的。但原始字符串语法允许您在表示字符串开头的"和(之间添加其他字符,这意味着表示字符串结尾的"和)之间也必须包含这些字符。因此,使用R"+*(标识原始字符串的开头时,必须使用)+*"标识原始字符串的结尾。因此,下面的语句:
将显示如下内容:
总之,这使用"+*(和)+*"替代了默认定界符"(和)"。自定义定界符时,在默认定界符之间添加任意数量的基本字符,但空格、左括号、右括号、斜杠和控制字符(如制表符和换行符)除外。
可将前缀R与其他字符串前缀结合使用,以标识wchar_t等类型的原始字符串。可将R放在前面,也可将其放在后面,如Ru、UR等。
下面介绍另一种复合类型—结构。