《C++标准程序库》笔记之三

时间:2021-07-27 19:52:26

本篇博客笔记顺序大体按照《C++标准程序库(第1版)》各章节顺序编排。

--------------------------------------------------------------------------------------------

11 Strings 字符串

本章单独列出,讲述C++标准程序库中的string(字符串)型别,包括针对基本template class basic_string<>及其标准特化型别string和wstring的详细内容。 C++标准程序库中的string class 使你可以将string当做一个一般型别而不会令用户感觉有任何问题。你可以像对待基本型别那样地复制、赋值和比较string,再也不必担心内存是否足够、占用的内存实际长度等问题,只需运用操作符操作函数即可。简而言之,C++标准程序库对于string的设计思维就是,让它的行为尽可能像基本型别,不会在操作上引起什么麻烦。

11.1

以下语句:

strring::size_type idx = filename.find('.');

(1)在字符串filename中搜寻第一个'.'字符。有好几个函数可以在字符串内实施搜寻功能,函数find()是其中之一。你也可以从后向前搜寻,或是搜寻子字符串,或是在字符串的某个范围内搜寻,或是同时搜寻数个字符。所有这些搜寻函数都返回第一个匹配位置(一个索引)。没错,返回值是个整数,而不是迭代器。字符串的一般接口并不依赖STL概念。然而字符串的确也提供了数种迭代器。所有搜寻函数的返回型别都是string::size_type,这是string class 定义的一个无正负号整数型别。当然啦,第一个字符的索引值为0,最后一个字符的索引值是numberOfCharacters-1.注意,numberOfCharacters并不是一个有效索引。和C-string不同,string对象的字符串尾部并没有一个特殊字符'\0'.

(2)如果搜寻失败,必须返回一个特殊值来表示,该值就是npos,定义于string class 中。如下面这一行语句用来检验搜寻动作是否失败: if (idx == string::npos) 注意,当你打算检验搜寻函数的返回值时,应该使用string::size_type型别而不是int或unsigned。否则上述与string::npos的比较动作将无法有效运行。

11.2
(1)string型别和wstring型别
C++标准程序库提供了两个basic_string<>特化版本:
1. string是针对char而预先定义的特化版本:

namespace std
{
    typedef basic_string<char> string;
}

2. wstring 是针对wchar_t而预先定义的特化版本:

namespace std
{
    typedef basic_string<wchar_t> wstring;
}

(2)操作函数(Operations)

表11.1 列出针对字符串而设计的所有操作函数

《C++标准程序库》笔记之三

(3)string的构造函数和析构函数(Constructors and Destructors)

《C++标准程序库》笔记之三

(4)Strings和C-Strings C++ Standard 将字符串字面常数的型别由char* 改为const char*。为了提供向下兼容性,C++ Standard规定了一个颇有争议的隐式转换,可从const char*隐式转换为char*。由于字符串字面常数的型别并非string,因此在新的string object和传统的C-strings之间必须存在一种强烈关系:在“strings和string-like object共通的操作场合”(例如比较、追加、插入等等动作)都应该可以使用C-strings(也即,对于string object提供支持的函数,也都应该相应的支持C-string)。或者具体地说,存在一个从const char* 到strings的隐式型别转换。然而却不存在一个从string object 到C-string的自动型别转换。这是出于安全考虑,防止意外转型导致奇异行为(char*经常有奇异的行为)和模棱两可(例如在一个结合了string和C-string的表达式中,既可以把string转化为char*,也可以反其道而行,这就导致模棱两可)。有好几种办法可以产生或改写/复制C-string。更明确地说,c_str()可以得到“string 对应的C-string”,所得结果和“以‘\0’为结尾的字符数组”一样。运用copy(),你也可以将字符串内容复制或写入既有的C-string或字符数组内。

请注意:

1. ‘\0’ 在string之中并不具有特殊意义,但在一般C-string中却用来标识字符串结束。在string中,字符‘\0’和其它字符的地位完全相同。

2. 千万不要以null指标(NULL)取代char*作为参数,这样会导致奇异行为,因为NULL具有整数型别,在单整数型别的重载函数版本上会被解释为数字0或“其值为0”的字符。

有三个函数可以将字符串内容转换为字符数组或C-string:
1. data() 以字符数组的形式返回字符串内容。由于并未追加‘\0’字符,所以返回型别并非有效的C-string。
2. c_str() 以C-string形式返回字符串内容,也就是在尾端添加‘\0’字符。
3. copy() 将字符串内容复制到“调用者提供的字符数组”中。不添加‘\0’字符。

std::string s("123456");
atoi(s.c_str());
f(s.data(), s.length());

char buffer(100);
s.copy(buffer, 100);
s.copy(buffer, 100, 2);

注意,data() 和 c_str() 返回的字符数组由该字符串拥有。也就是说调用者千万不可修改它或释放其内存。例如:

std::string s;
...
foo(s.c_str()); // s.c_str() is valid during the whole statement

const char* p;
p = s.c_str();      // p refers to the contents of s as a C-string p
foo(p);        // OK(p is still valid)
s += "ext";     // invalidates p
foo(p);        // ERROR:argument p is not valid

一般而言,整个程序中你应该坚持使用strings,直到你必须将其内容转化为char*时才把它们转换为C-string。请注意c_str()和data()的返回值有效期限在下一次调用non-const成员函数时即告终止。

让string拥有足够的容量是很重要的,原因有二:

1. 重新分配会造成所有指向string的references、pointers和iterators失效。

2. 重新分配很耗时间。

容量概念应用于string和应用于vector是相同的;但有一个显著差异:面对string你可以调用reserve()来缩减实际容量,而vector的reserve()却没有这项功能。string的reserve()是一种非强制性适度缩减请求——不保证一定可以如愿。C++ Standard 规定,唯有在响应reserve() 调用时,容量才有可能缩减。因此即使发生“字符被删除或被改变”的事情,任何其他字符只要位于“被操作字符”之前,指向它们身上的那些references、pointers和iterators就仍然保持有效。

(5)元素存取(Element Access)

String有两种方法可以访问单一字符:subscript(下标)操作符[] 和 成员函数 at()。但有两点区别:

1. operator [] 并不检查索引是否有效,at() 则会检查。如果at()指定的索引无效,系统会抛出out_of_range异常。如果调用operator[]指定的索引无效,其行为未有定义——可能存取非法内存,引起边缘效应或崩溃。

2. 对于 operator[] 的const 版本,最后一个字符的后面位置也是有效的。此时的实际字符数是有效索引。在此情况下 operator[] 的返回值是“由char型别值default构造函数所产生”的字符。因此,对于型别为string的对象,返回值为“\0”字符。

其他任何情况(包括成员函数at() 和 operator[] 的non-const 版本),实际字符数都是个无效索引。如果使用该索引,会引发异常,或导致未定义行为。下例:

const std::string cs("nico");   // cs contains: 'n' 'i' 'c' 'o'
std::string s("abcde");      // s contains: 'a' 'b' 'c' 'd' 'e'
 s[2]     // yields 'c'
 s.at(2)       // yields 'c'

 s[100]      // ERROR:undifined behavior
 s.at(100)    // throws out_of_range

s.[s.length()]        // ERROR:undefined behavior
cs[cs.length()]     // yields '\0'
s.at(s.length())        // throw out_of_range
cs.at(cs.length())      // throw out_of_range

为了允许更改string内容,operator[] 的 non-const 版本和at() 都返回字符的reference。一旦发生重分配行为,那些reference立即失效:

std::string s("abcde");     // s contains: 'a' 'b' 'c' 'd' 'e'

char& r = s[2];         // reference to third character
char* p = &s[3];         // pointer to fourth character

r = 'X';         // OK, s contains: 'a' 'b' 'X' 'd' 'e'
*p = 'Y';         // OK, s contains: 'a' 'b' 'X' 'Y' 'e'

s = "new long value";          // realloction invalidates r and p

r = 'X';             // ERROR: undefined behavior
*p = 'Y';             // ERROR: undefined behavior

(6)I/O操作符 ">>", "<<"

String classes 在命名空间std内还提供了一种用于逐行读取的特殊函数:std::getline() 。该函数读取所有字符,包括开头的空格符,直到遭遇分行符号或end_of_file。分行符号可指定,不可添加。缺省情况下分行符号为newline字符。

(7)表11.5 列出来了Strings搜寻函数。

《C++标准程序库》笔记之三

注意,所有搜寻函数都返回符合搜寻条件之字符区间内的第一个字符的索引。如果搜寻不成功,则返回npos,其类型为 std::string::size_type。 不能以 int 或 unsigned 作为返回值型别;否则返回值与string::npos之间的比较可能无法正确执行。这是因为npos在basic_string类模板里面被设计为-1。不幸的是size_type(由字符串配置器allocator定义)需为无正负号整数型别。因为缺省配置器以型别size_t作为size_type。于是 -1 被转换为无正负号整数型别,npos也就成了该型别的最大无符号值。

(8)Strings对迭代器的支持

1. string是字符的有序群集,所以C++标准程序库为strings提供了相应接口,以便将字符串当做STL容器使用。

2. String迭代器是random access(随机存取)迭代器。

3. 对迭代器而言,如果发生重分配(reallocation),或其所指值发生某些变化,迭代器就会失效。

表11.6 提供了一份迭代器操作函数列表

《C++标准程序库》笔记之三

(9)Strings 和 Vectors

两者都是一种动态数组。因此可以把strings视为一种“以字符作为元素”的特定vectors。实用上可把string当作STL容器使用。当两者的主要差异在于各有目标:

1. vectors 首要目标是处理和操作容器内的元素,而非容器整体。因此实作时通常会为“容器元素的操作行为”进行优化。

2. strings 主要是把整个容器视为整体,进行处理和操作,因此实作时通常会为“整个容器的赋值和传递”进行优化。

不同的目标导致完全不同的实作手法。例如strings 通常采用reference counting(这种手法可以加速string的复制和赋值)手法,vectors则决不会如此。