0%

C++ Primer - 第 3 章 字符串、向量和数组

  1. using 声明(using declaration)。有了 using 声明就无须专门的前缀(形如命名空间::)也能使用所需的名字了。using 声明具有如下的形式:

    1
    using namespace::name;

    一旦声明了上述语句,就可以直接访问命名空间中的名字:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include <iostream>
    // using声明,当我们使用名字cin时,从命名空间std中获取它
    using std::cin;
    int main()
    {
    int i;
    cin >> i; // 正确:cin和std::cin含义相同
    cout << i; // 错误:没有对应的using声明,必须使用完整的名字
    std::cout << i; // 正确:显式地从std中使用cout
    return 0;
    }
  2. 位于头文件的代码一般来说不应该使用 using 声明。因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个 using 声明,那么每个使用了该头文件的文件就都会有这个声明。对于某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突。

  3. 标准库类型 string 表示可变长的字符序列,使用 string 类型必须首先包含 string 头文件。作为标准库的一部分,string 定义在命名空间 std 中:

    1
    2
    #include <string>
    using std::string;
  4. 一个类可以定义很多种初始化对象的方式,只不过这些方式之间必须有所区别:或者是初始值的数量不同,或者是初始值的类型不同。下表列出了初始化 string 对象最常用的一些方式:

    写法 含义
    string s1 默认初始化,s1是一个空串
    string s2(s1) s2是s1的副本
    string s2 = s1 等价于s2(s1),s2是s1的副本
    string s3("value") s3是字面值“value”的副本,除了字面值最后的那个空字符外
    string s3 = "value" 等价于s3("value"),s3是字面值“value”的副本
    string s4(n, 'c') 把s4初始化为由连续n个字符c组成的串

    下面是几个例子:

    1
    2
    3
    4
    string s1;           // 默认初始化,s1是一个空字符串
    string s2 = s1; // s2是s1的副本
    string s3 = "hiya"; // s3是该字符串字面值的副本
    string s4(10, 'c'); // s4的内容是cccccccccc
  5. 如果使用等号(=)初始化一个变量,实际上执行的是拷贝初始化(copy initialization),编译器把等号右侧的初始值拷贝到新创建的对象中去。与之相反,如果不使用等号,则执行的是直接初始化(direct initialization)。当初始值只有一个时,使用直接初始化或拷贝初始化都行。如果像上面的 s4 那样初始化要用到的值有多个,一般来说只能使用直接初始化的方式:

    1
    2
    3
    string s5 = "hiya";  // 拷贝初始化
    string s6("hiya"); // 直接初始化
    string s7(10, 'c'); // 直接初始化,s7的内容是cccccccccc

    对于用多个值进行初始化的情况,也可以使用拷贝初始化的方式,不过需要显式地创建一个(临时)对象用于拷贝:

    1
    string s8 = string(10, 'c');  // 拷贝初始化,s8的内容是cccccccccc
  6. string 对象可进行的大多数操作如下表所示:

    string的操作
    os << s 将s写到输出流os当中,返回os
    is >> s 从is中读取字符串赋给s,字符串以空白分隔,返回is
    getline(is, s) 从is中读取一行赋给s,返回is
    s.empty() s为空返回true,否则返回false
    s.size() 返回s中字符的个数
    s[n] 返回s中第n个字符的引用,位置n从0计起
    s1 + s2 返回s1和s2连接后的结果
    s1 = s2 用s2的副本代替s1中原来的字符
    s1 == s2 如果s1和s2中所含的字符完全一样,则它们相等;string对象的相等性判断对字母的大小写敏感
    s1 != s2
    <, <=, >, >= 利用字符在字典中的顺序进行比较,且对字母的大小写敏感
  7. 观察下述代码片段:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    int main()
    {
    string s; // 空字符串
    cin >> s; // 将string对象读入s,遇到空白为止
    cout << s << endl; // 输出s

    string s1, s2;
    cin >> s1 >> s2; // 把第一个读入到s1中,第二个读入到s2中
    cout << s1 << s2 << endl;

    return 0;
    }

    在执行读取操作时,string 对象会自动忽略开头的空白(即空格符、换行符、制表符等)并从第一个真正的字符开始读起,直到遇见下一处空白为止。将上述代码编译为可执行程序,在终端中运行可执行程序,输入 Hello World! 后回车,将打印此时 s 的值 Hello,可知输入的空白被忽略了;再输入 ha ya 后回车,将打印 World!ha,这是因为第一次的输入中的 World! 还在键盘缓冲区,在执行

    1
    cin >> s1 >> s2;

    时,键盘缓冲区中的内容被读到了 s1,然后第二次输入中的 ha 被读到了 s2,ya 又被暂存在了键盘缓冲区。

  8. 观察下述代码片段:

    1
    2
    3
    4
    5
    6
    7
    int main()
    {
    string word;
    while (cin >> word)
    cout << word << endl;
    return 0;
    }

    while 的循环条件负责在读取时检测流的情况,如果流有效,也就是说没遇到文件结束标记或非法输入,那么执行 while 语句内部的操作。此时,循环体将输出刚刚从标准输入读取的内容。重复若干次之后,一旦遇到文件结束标记或非法输入循环也就结束了。cin 读取的内容会被暂存在键盘缓冲区。

  9. getline 函数的参数是一个输入流和一个 string 对象,函数从给定的输入流中读入内容,直到遇到换行符为止(注意换行符也被读进来了),然后把所读的内容存入到那个 string 对象中去(注意不存换行符)。getline 只要一遇到换行符就结束读取操作并返回结果,哪怕输入的一开始就是换行符也是如此。如果输入真的一开始就是换行符,那么所得的结果是个空 string。和输入运算符一样,getline 也会返回它的流参数。因此既然输入运算符能作为判断的条件,也能用 getline 的结果作为条件。观察下述代码片段:

    1
    2
    3
    4
    5
    6
    7
    int main()
    {
    string line;
    while (getline(cin, line))
    cout << line << endl;
    return 0;
    }

    触发 getline 函数返回的那个换行符实际上被丢弃掉了,得到的 string 对象中并不包含该换行符。输入完一行的内容后回车,getline 本次读取结束,执行 while 主体中的语句,再开始执行下一次的 getline 读取操作,直至输入 Ctrl + z(Windows 系统下)或 Ctrl + d(类 Unix 系统下)并回车才退出循环。

  10. string 类中的 empty 函数根据 string 对象是否为空返回一个对应的布尔值。

  11. string 类中的 size 函数返回 string 对象的长度(即 string 对象中字符的个数)。size 函数返回的是一个 string::size_type 类型的值。所有用于存放 string 类的 size 函数返回值的变量,都应该是 string::size_type 类型的。在 C++11 新标准中,允许编译器通过 auto 或者 decltype 来推断变量的类型:

    1
    auto len = line.size();  // len 的类型是 string::size_type

    由于 size 函数返回的是一个无符号整型数,因此切记,如果在表达式中混用了带符号数和无符号数将可能产生意想不到的结果。例如,假设 n 是一个具有负值的 int,则表达式 s.size() < n 的判断结果几乎肯定是 true。这是因为负值 n 会自动地转换成一个比较大的无符号值。如果一条表达式中已经有了 size() 函数就不要再使用 int 了,这样可以避免混用 intunsigned 可能带来的问题。

  12. string 类定义了几种用于比较字符串的运算符。这些比较运算符逐一比较 string 对象中的字符,并且对大小写敏感,也就是说,在比较时同一个字母的大写形式和小写形式是不同的。相等性运算符(==!=)分别检验两个 string 对象相等或不相等,string 对象相等意味着它们的长度相同而且所包含的字符也全都相同。关系运算符 <<=>>= 分别检验一个 string 对象是否小于、小于等于、大于、大于等于另外一个 string 对象。上述这些运算符都依照(大小写敏感的)字典顺序:

    • 规则一:如果两个 string 对象的长度不同,而且较短 string 对象的每个字符都与较长 string 对象对应位置上的字符相同,就说较短 string 对象小于较长 string 对象;
    • 规则二:如果两个 string 对象在某些对应的位置上不一致,则 string 对象比较的结果其实是 string 对象中第一对相异字符比较的结果。

    下面是 string 对象比较的一个示例:

    1
    2
    3
    string str = "Hello";
    string phrase = "Hello World";
    string slang = "Hiya";

    根据规则一可判断,对象 str 小于对象 phrase;根据规则二可判断,对象 slang 既大于 str 也大于 phrase。

  13. 对于 string 类而言,允许把一个对象的值赋给另外一个对象:

    1
    2
    string st1(10,'c'), st2;
    st1 = st2;
  14. 两个 string 对象相加得到一个新的 string 对象,其内容是把左侧的运算对象与右侧的运算对象串接而成。复合赋值运算符(+=)负责把右侧 string 对象的内容追加到左侧 string 对象的后面

    1
    2
    3
    string s1 = "hello, ", s2 = "world\n";
    string s3 = s1 + s2; // s3的内容是hello,world\n
    s1 += s2; // 等价于s1 = s1 + s2
  15. 标准库允许把字符字面值和字符串字面值转换成 string 对象,所以在需要 string 对象的地方就可以使用这两种字面值来替代:

    1
    2
    string s1 = "hello", s2 = "world";  // 在s1和s2中都没有标点符号
    string s3 = s1 + ", " + s2 + '\n';

    当把 string 对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符(+)的两侧的运算对象至少有一个是 string

    1
    2
    3
    4
    5
    string s4 = s1 + ", ";       // 正确:把一个string对象和一个字面值相加
    string s5 = "hello" + ", "; // 错误:两个运算对象都不是string
    //正确:每个加法运算符都有一个运算对象是string
    string s6 = s1 + ", " + "world";
    string s7 = "hello" + ", " + s2; //错误:不能把字面值直接相加

    s6 的初始化形式的工作机理和连续输入连续输出是一样的,可以用如下的形式分组:

    1
    string s6 = (s1 + ", ") + "world";

    其中子表达式 s1 + ", ” 的结果是一个 string 对象,它同时作为第二个加法运算符的左侧运算对象,因此上述语句和下面的两个语句是等价的:

    1
    2
    string tmp = s1 + ", ";  // 正确:加法运算符有一个运算对象是string
    s6 = tmp + "world"; // 正确:加法运算符有一个运算对象是string

    另一方面,s7 的初始化是非法的,根据其语义加上括号后就成了下面的形式:

    1
    string s7 = ("hello" + ", ") + s2;  // 错误:不能把字面值直接相加

    很容易看到,括号内的子表达式试图把两个字符串字面值加在一起,而编译器根本没法做到这一点,所以这条语句是错误的。

  16. C++ 语言中的字符串字面值并不是标准库类型 string 的对象。切记,字符串字面值与 string 是不同的类型。

  17. cctype 头文件中定义了一组标准库函数用于单独处理 string 对象中的字符,其中主要的函数名及其含义如下表所示:

    cctype头文件中的函数
    isalnum(c) 当c是字母或数字时为真
    isalpha(c) 当c是字母时为真
    iscntrl(c) 当c是控制字符时为真
    isdigit(c) 当c是数字时为真
    isgraph(c) 当c不是空格但可打印时为真
    islower(c) 当c是小写字母时为真
    isprint(c) 当c是可打印字符时为真(即c是空格或c具有可视形式)
    ispunct(c) 当c是标点符号时为真(即c不是控制字符、数字、字母、可打印空白中的一种)
    isspace(c) 当c是空白时为真(即c是空格、横向制表符、纵向制表符、回车符、换行符、进纸符中的一种)
    isupper(c) 当c是大写字母时为真
    isxdigit(c) 当c是十六进制数字时为真
    tolower(c) 如果c是大写字母,输出对应的小写字母;否则原样输出c
    toupper(c) 如果c是小写字母,输出对应的大写字母;否则原样输出c
  18. 建议:使用 C++ 版本的 C 标准库头文件。C++ 标准库中除了定义 C++ 语言特有的功能外,也兼容了 C 语言的标准库。C 语言的头文件形如 name.h,C++ 则将这些文件命名为 cname。也就是去掉了 .h 后缀,而在文件名 name 之前添加了字母 c,这里的 c 表示这是一个属于 C 语言标准库的头文件。因此,cctype 头文件和 ctype.h 头文件的内容是一样的,只不过从命名规范上来讲更符合 C++ 语言的要求。特别的,在名为 cname 的头文件中定义的名字从属于命名空间 std,而定义在名为 .h 的头文件中的则不然。 一般来说,C++ 程序应该使用名为cname 的头文件而不使用 name.h 的形式,标准库中的名字总能在命名空间 std 中找到。

  19. 如果想对 string 对象中的每个字符做点儿什么操作,使用 C++11 新标准提供的一种语句:范围 for(range for)语句。这种语句遍历给定序列中的每个元素并对序列中的每个值执行某种操作,其语法形式是:

    1
    2
    for (declaration : expression)
    statement

    其中,expression 部分是一个对象,用于表示一个序列。declaration 部分负责定义一个变量,该变量将被用于访问序列中的基础元素。每次迭代,_declaration_ 部分的变量会被初始化为 expression 部分的下一个元素值。一个 string 对象表示一个字符的序列,因此 string 对象可以作为范围 for 语句中的 expression 部分。

  20. 下面的代码片段通过使用范围 for 语句把 string 对象中的字符每行一个输出出来:

    1
    2
    3
    string str("some string");
    for (auto c : str)
    cout << c << endl;

    通过使用 auto 关键字让编译器来决定变量 c 的类型,这里 c 的类型是 char。每次迭代,str 的下一个字符被拷贝给 c,因此该循环可以读作“对于字符串 str 中的每个字符 c,”执行某某操作。

  21. 下面的代码片段使用范围 for 语句和 ispunct 函数来统计 string 对象中标点符号的个数:

    1
    2
    3
    4
    5
    6
    7
    string s("Hello World!!!");
    decltype punct_cnt = 0;
    for (auto c : s)
    if (ispunct(c))
    ++punct_cnt;
    cout << punct_cnt
    << " punctuation characters in " << s << endl;
  22. 使用范围 for 语句改变字符串中的字符。如果想要改变 string 对象中字符的值,必须把循环变量定义成引用类型。记住,所谓引用只是给定对象的一个别名,因此当使用引用作为循环控制变量时,这个变量实际上被依次绑定到了序列的每个元素上。使用这个引用,我们就能改变它绑定的字符。标准库函数 toupper 接收一个字符,然后输出其对应的大写形式。这样,为了把整个 string 对象转换成大写,只要对其中的每个字符调用 toupper 函数并将结果再赋给原字符就可以了:

    1
    2
    3
    4
    5
    string s("Hello World!!!");
    // 转换成大写形式。
    for (auto &c : s) // 对于s中的每个字符(注意:c是引用
    c = toupper(c); // c是一个引用,因此赋值语句将改变s中字符的值
    cout << s << endl;

    每次迭代时,变量 c 引用string对象 s 的下一个字符,赋值给 c 也就是在改变 s 中对应字符的值。因此当执行下面的语句时,

    1
    c = toupper(c);   // c是一个引用,因此赋值语句将改变s中字符的值

    实际上改变了 c 绑定的字符的值。整个循环结束后,str 中的所有字符都变成了大写形式。

  23. 要想访问 string 对象中的单个字符有两种方式:一种是使用下标,另外一种是使用迭代器。下标运算符([ ])接收的输入参数是 string::size type 类型的值,这个参数表示要访问的字符的位置;返回值是该位置上字符的引用。string 对象的下标从 0 计起,s[s.size()-1] 是最后一个字符。string 对象的下标必须大于等于 0 而小于 s.size()。使用超出此范围的下标将引发不可预知的结果,以此推断,使用下标访问空 string 也会引发不可预知的结果。下标的值称作“下标”或“索引”,任何表达式只要它的值是一个整型值就能作为索引。不过,如果某个索引是带符号类型的值将自动转换成 string::size_type 表达的无符号类型。在访问指定字符之前,首先检查 s 是否为空。其实不管什么时候只要对 string 对象使用了下标,都要确认在那个位置上确实有值。如果 s 为空,则 s[0] 的结果将是未定义的。只要字符串不是常量,就能为下标运算符返回的字符赋新值。下面的程序将字符串的首字符改成了大写形式:

    1
    2
    3
    strings("some string");
    if (!s.empty()) // 确保s[0]的位置确实有字符
    s[0] = toupper(s[0]); // 为s的第一个字符赋一个新值

    下面的程序把 s 的第一个词改成大写形式:

    1
    2
    3
    4
    5
    // 依次处理s中的字符直至处理完全部字符或者遇到一个空白
    for (decltype(s.size()) index = 0;
    index != s.size() && !isspace(s[index]);
    ++index)
    s[index] = toupper(s[index]); // 将当前字符改成大写形式
  24. C++ 语言规定只有当左侧运算对象为真时才会检查右侧运算对象的情况(C 语言中有着相同规定),这条规定确保了只有当下标取值在合理范围之内时才会真的用此下标去访问字符串。C++ 标准并不要求标准库检测下标是否合法,一旦使用了一个超出范围的下标,就会产生不可预知的结果。

  25. 标准库类型 vector 表示对象的集合,其中所有对象的类型都相同。集合中的每个对象都有一个与之对应的索引,索引用于访问对象。vector 常被称作容器(container)。要想使用 vector,必须包含适当的头文件:

    1
    2
    #include <vector>
    using std::vector;
  26. C++ 语言既有类模板(class template),也有函数模板,其中 vector 是一个类模板。模板本身不是类或函数,相反可以将模板看作为编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程称为实例化(instantiation),当使用模板时,需要指出编译器应把类或函数实例化成何种类型。对于类模板来说,通过提供一些额外信息来指定模板到底实例化成什么样的类,需要提供哪些信息由模板决定。提供信息的方式总是这样:即在模板名字后面跟一对尖括号,在括号内放上信息。以 vector 为例,提供的额外信息是 vector 内所存放对象的类型:

    1
    2
    3
    vector<int> ivec;              // ivec保存int类型的对象
    vector<Sales_item> Sales vec; // 保存Sales_item类型的对象
    vector<vector<string>> file; // 该向量的元素是vector对象
  27. vector 是模板而非类型,由 vector 生成的类型必须包含 vector 中元素的类型,例如 vector<int>

  28. vector 能容纳绝大多数类型的对象作为其元素,但是因为引用不是对象,所以不存在包含引用的vector。除此之外,其他大多数(非引用)内置类型和类类型都可以构成 vector 对象,甚至组成 vector 的元素也可以是 vector

  29. 在早期版本的 C++ 标准中,如果 vector 的元素还是 vector(或者其他模板类型),则其定义的形式与现在的 C++11 新标准略有不同。过去,必须在外层 vector 对象的右尖括号和其元素类型之间添加一个空格,如应该写成 vector<vector<int> > 而非 vector<vector<int>>

  30. 定义 vector 对象的常用方法如下表所示:

    初始化vector对象的方法
    vector<T> v1 v1是一个空vector,它潜在的元素是T类型的,执行默认初始化
    vector<T> v2(v1) v2中包含有v1所有元素的副本
    vector<T> v2 = v1 等价于v2(v1),v2中包含有v1所有元素的副本
    vector<T> v3(n, val) v3包含了n个重复的元素,每个元素的值都是val
    vector<T> v4(n) v4包含了n个重复地执行了值初始化的对象
    vector<T> v5{a, b, c ...} v5包含了初始值个数的元素,每个元素被赋予相应的初始值
    vector<T> v5 = {a, b, c ...} 等价于v5{a, b, c...}
  31. 可以默认初始化 vector对象,从而创建一个指定类型的空 vector

    1
    vector<string> svec;  // 默认初始化,svec不含任何元素
  32. 允许把一个 vector 对象的元素拷贝给另外一个 vector 对象。此时,新 vector 对象的元素就是原 vector 对象对应元素的副本。注意两个 vector 对象的类型必须相同:

    1
    2
    3
    4
    5
    vector<int> ivec;  // 初始状态为空
    // 在此处给ivec添加一些值
    vector<int> ivec2(ivec); // 把ivec的元素拷贝给ivec2
    vector<int> ivec3 = ivec; // 把ivec的元素拷贝给ivec3
    vector<string> svec(ivec2); // 错误:svec的元素是string对象,不是int
  33. C++11 新标准还提供了另外一种为 vector 对象的元素赋初值的方法,即列表初始化。此时,用花括号括起来的 0 个或多个初始元素值被赋给 vector 对象:

    1
    vector<string> articles = {"a", "an", "the"};
  34. 使用 C++ 中几种不同的初始化方式时的三点要求:

    • 其一,使用拷贝初始化时(即使用=时),只能提供一个初始值;
    • 其二,如果提供的是一个类内初始值,则只能使用拷贝初始化或使用花括号的形式初始化;
    • 其三,如果提供的是初始元素值的列表,则只能把初始值都放在花括号里进行列表初始化,而不能放在圆括号里。

    1
    2
    vector<string> v1{"a", "an", "the"};  // 列表初始化
    vector<string> v2("a", "an", "the"); // 错误
  35. 还可以用 vector 对象容纳的元素数量和所有元素的统一初始值来初始化 vector 对象:

    1
    2
    vector<int> ivec(10, -1);        // 10个int类型的元素,每个都被初始化为-1
    vector<string> svec(10, "hi!"); // 10个string类型的元素,每个都被初始化为"hi!"
  36. 通常情况下,可以只提供 vector 对象容纳的元素数量而略去初始值。此时库会创建一个值初始化的(value-initialized)元素初值,并把它赋给容器中的所有元素。这个初值由 vector 对象中元素的类型决定。如果 vector 对象的元素是内置类型,比如 int,则元素初始值自动设为 0。如果元素是某种类类型,比如 string,则元素由类默认初始化:

    1
    2
    vector<int> ivec(10);     // 10个元素,每个都初始化为0
    vector<string> svec(10); // 10个元素,每个都是空string对象

    对这种初始化的方式有两个特殊限制:、

    • 其一,有些类要求必须明确地提供初始值,如果 vector 对象中元素的类型不支持默认初始化,就必须提供初始的元素值。对这种类型的对象来说,只提供元素的数量而不设定初始值无法完成初始化工作;
    • 其二,如果只提供了元素的数量而没有设定初始值,只能使用直接初始化:

    1
    vector<int> vi = 10;  // 错误:必须使用直接初始化的形式指定向量大小
  37. 在某些情况下,初始化的真实含义依赖于传递初始值时用的是花括号还是圆括号。 观察如下代码片段:

    1
    2
    3
    4
    vector<int> v1(10);     // v1有10个元素,每个的值都是0
    vector<int> v2{10}; // v2有1个元素,该元素的值是10
    vector<int> v3(10, 1); // v3有10个元素,每个的值都是1
    vector<int> v4{101}; // v4有2个元素,值分别是10和1

    如果用的是花括号,可以表述成我们想列表初始化(list initialize)该 vector 对象。也就是说,初始化过程会尽可能地把花括号内的值当成是元素初始值的列表来处理,只有在无法执行列表初始化时才会考虑其他初始化方式。另一方面,如果初始化时使用了花括号的形式但是提供的值又不能用来列表初始化,就要考虑用这样的值来构造 vector 对象了

    1
    2
    3
    4
    vector<string> v5{"hi"};      // 列表初始化:v5有一个元素
    vector<string> v6("hi"); // 错误:不能使用字符串字面值构建vector对象
    vector<string> v7{10}; // v7有10个默认初始化的元素
    vector<string> v8{10, "hi"}; // v8有10个值为“hi”的元素

    尽管在上面的例子中除了第二条语句之外都用了花括号,但其实只有 v5 是列表初始化。要想列表初始化 vector 对象,花括号里的值必须与元素类型相同。显然不能用 int 初始化 string 对象,所以 v7 和 v8 提供的值不能作为元素的初始值。确认无法执行列表初始化后,编译器会尝试用默认值初始化 vector 对象。

  38. 下列的 vector 对象各包含多少个元素?这些元素的值分别是多少?

    1
    2
    3
    4
    5
    6
    7
    vector<int> v1;               // (a):0个元素
    vector<int> v2(10); // (b):10个0
    vector<int> v3(10, 42); // (c):10个42
    vector<int> v4{10}; // (d):1个10
    vector<int> v5{10, 42}; // (e):2个,10和42
    vector<string> v6{10}; // (f):10个空字符串
    vector<string> v7{10, "hi"}; // (g):10个"hi"
  39. vector 的成员函数 push_back 向其中添加元素。push_back 负责把一个值当成 vector 对象的尾元素“压到(push)”vector 对象的“尾端(back)”。例如:

    1
    2
    3
    4
    vector<int> v2;  // 空vector对象
    for (int i = 0; i != 100; ++i)
    v2.push_back(i); // 依次把整数值放到v2尾端
    // 循环结束后v2有100个元素,值从0到99
  40. vector 对象能高效地增长,那么在定义 vector 对象的时候设定其大小也就没什么必要了,事实上如果这么做性能可能更差。只有一种例外情况,就是所有(all)元素的值都一样。一旦元素的值有所不同,更有效的办法是先定义一个空的 vector 对象,再在运行时向其中添加具体值。

  41. 如果循环体内部包含有向 vector 对象添加元素的语句,则不能使用范围 for 循环,范围 for 语句体内不应改变其所遍历序列的大小。

  42. 比较重要的一些 vector 支持的操作如下表所示:

    vector支持的操作
    v.empty() 如果v不含有任何元素,返回真;否则返回假
    v.size() 返回v中元素的个数
    v.push_back(t) 向v的尾端添加一个值为t的元素
    v[n] 返回v中第n个位置上元素的引用
    v1 = v2 用v2中元素的拷贝替换v1中的元素
    v1 = {a, b, c ..} 用列表中元素的拷贝替换v1中的元素
    v1 == v2 v1和v2相等当且仅当它们的元素数量相同且对应位置的元素值都相同
    v1 != v2
    <, <=, >, >= 以字典顺序进行比较
  43. 可以使用范围 for 语句处理 vector 对象中的所有元素:

    1
    2
    3
    4
    5
    6
    vector<int> v{1, 2, 3, 4, 5, 6, 7, 8, 9};
    for (auto &i : v) // 对于v中的每个元素(注意:i是一个引用)
    i *= i; // 求元素值的平方
    for (auto i : v) // 对于v中的每个元素
    cout << i << " "; // 输出该元素
    cout << endl;
  44. vectorsize 函数返回 vector 对象中元素的个数,返回值的类型是由 vector 定义的 size_type 类型。要使用 size_type,需首先指定它是由哪种类型定义的。vector 对象的类型总是包含着元素的类型:

    1
    2
    vector<int>::size_type  // 正确
    vector::size_type // 错误
  45. 两个 vector 对象相等当且仅当它们所含的元素个数相同,而且对应位置的元素值也相同。关系运算符依照字典顺序进行比较:如果两个 vector 对象的容量不同,但是在相同位置上的元素值都一样,则元素较少的 vector 对象小于元素较多的 vector 对象;若元素的值有区别,则 vector 对象的大小关系由第一对相异的元素值的大小关系决定。

  46. 只要 vector 对象不是一个常量,就能向下标运算符返回的元素赋值。

  47. 两个整数相除,结果还是整数,余数部分被自动忽略掉了。下面的程序以 10 分为一个分数段统计成绩的数量:0~9,10~19…,90~99,100:

    1
    2
    3
    4
    5
    6
    7
    vector<unsigned> scores(11, 0);  // 11个分数段,全都初始化为0
    unsigned grade;
    while (cin >> grade) // 读取成绩
    {
    if (grade <= 100) // 只处理有效的成绩
    ++scores[grade/10]; // 将对应分数段的计数值加1
    }
  48. 下面的代码试图为 vector 对象 ivec 添加 10 个元素:

    1
    2
    3
    vector<int> ivec;  // 空vector对象
    for (decltype(ivec.size()) ix = 0; ix != 10; ++ix)
    ivec[ix] = ix; // 严重错误:ivec不包含任何元素

    然而,这段代码是错误的:ivec 是一个空 vector,根本不包含任何元素,当然也就不能通过下标去访问任何元素!如前所述,正确的方法是使用 push_backvector 对象(以及 string 对象)的下标运算符可用于访问已存在的元素,而不能用于添加元素。

  49. 试图用下标的形式去访问一个不存在的元素将引发错误,不过这种错误不会被编译器发现,而是在运行时产生一个不可预知的值。不幸的是,这种通过下标访问不存在的元素的行为非常常见,而且会产生很严重的后果。所谓的缓冲区溢出(buffer overflow)指的就是这类错误,这也是导致 PC 及其他设备上应用程序出现安全问题的一个重要原因。确保下标合法的一种有效手段就是尽可能使用范围 for 语句。

  50. 可以使用下标运算符来访问 string 对象的字符或 vector 对象的元素,使用迭代器(iterator)也可以做到这一点。所有标准库容器都可以使用迭代器,但是其中只有少数几种才同时支持下标运算符。

  51. 迭代器也提供了对对象的间接访问。就迭代器而言,其对象是容器中的元素或者 string 对象中的字符。使用迭代器可以访问某个元素,迭代器也能从一个元素移动到另外一个元素。迭代器有有效和无效之分,这一点和指针差不多。有效的迭代器或者指向某个元素,或者指向容器中尾元素的下一位置;其他所有情况都属于无效。

  52. 获取迭代器不是使用取地址符,有迭代器的类型同时拥有返回迭代器的成员。比如,这些类型都拥有名为 beginend 的成员,其中 begin 成员负责返回指向第一个元素(或第一个字符)的迭代器。end 成员则负责返回指向容器(或 string 对象)“尾元素的下一位置(one past the end)”的迭代器,也就是说,该迭代器指示的是容器的一个本不存在的“尾后(off the end)”元素。end 成员返回的迭代器常被称作尾后迭代器(off-the-end iterator)或者简称为尾迭代器(end iterator)。特殊情况下如果容器为空,则 beginend 返回的是同一个迭代器——尾迭代器。

  53. 下表列举了迭代器支持的一些运算。使用 ==!= 来比较两个合法的迭代器是否相等,如果两个迭代器指向的元素相同或者都是同一个容器的尾后迭代器,则它们相等:否则就说这两个迭代器不相等。

    标准容器迭代器的运算符
    *iter 返回迭代器iter所指元素的引用
    iter->mem 解引用iter并获取该元素的名为mem的成员,等价于(*iter).mem
    ++iter 令iter指示容器中的下一个元素
    --iter 令iter指示容器中的上一个元素
    iter1 == iter2 判断两个迭代器是否相等(不相等),如果两个迭代器指示的是同一个元素或者它们是同一个容器的尾后迭代器,则相等;反之,不相等
    iter1 != iter2

    和指针类似,也能通过解引用迭代器来获取它所指示的元素,执行解引用的迭代器必须合法并确实指示着某个元素。试图解引用一个非法迭代器或者尾后迭代器都是未被定义的行为。

  54. 下面的程序将 string 对象的第一个字母改为了大写形式:

    1
    2
    3
    4
    5
    6
    string s("some string");
    if (s.begin() != s.end())
    {
    auto it = s.begin();
    *it = toupper(*it);
    }
  55. 迭代器使用递增(++)运算符来从一个元素移动到下一个元素。因为 end 返回的迭代器并不实际指示某个元素,所以不能对其进行递增或解引用的操作。

  56. 下面的程序将 string 对象的第一个单词改为了大写形式:

    1
    2
    3
    string s("some string");
    for (auto it = s.begin(); it != s.end() && !isspace(*it); ++it)
    *it = toupper(*it);
  57. 只有 stringvector 等一些标准库类型有下标运算符,而并非全都如此。与之类似,所有标准库容器的迭代器都定义了 ==!=,但是它们中的太多数都没有定义 < 运算符。因此,只要养成使用迭代器和 != 的习惯,就不用太在意用的到底是哪种容器类型。

  58. 拥有迭代器的标准库类型使用 iteratorconst_iterator 来表示迭代器的类型:

    1
    2
    3
    4
    5
    vector<int>::iterator it;         // it能读写vector<int>的元素
    string::iterator it2; // it2能读写string对象中的元素

    vector<int>::const_iterator it3; // it3只能读元素,不能写元素
    string::const_iterator it4; // it4只能读字符,不能写字符

    const_iterator 和指向常量的指针差不多,能读取但不能修改它所指的元素值。相反,iterator 的对象可读可写。如果 vector 对象或 string 对象是一个常量,只能使用 const_iterator;如果 vector 对象或 string 对象不是常量,那么既能使用 iterator 也能使用 const_iterator

  59. 每个容器类定义了一个名为 iterator 的类型,该类型支持迭代器概念所规定的一套操作。

  60. beginend 返回的具体类型由对象是否是常量决定,如果对象是常量,beginend 返回 const_iterator;如果对象不是常量,返回 iterator

    1
    2
    3
    4
    vector<int> v;
    const vector<int> cv;
    auto it1 = v.begin(); // it1的类型是vector<int>::iterator
    auto it2 = cv.begin(); // it2的类型是vector<int>::const_iterator

    如果对象只需读操作而无须写操作的话最好使用常量类型(比如 const_iterator)。为了便于专门得到 const_iterator 类型的返回值,C++11 新标准引入了两个新函数,分别是 cbegincend

    1
    auto it3 = v.cbegin();  // it3的类型是vector<int>::const_iterator

    类似于 beginend,上述两个新函数也分别返回指示容器第一个元素或最后元素下一位置的迭代器。有所不同的是,不论 vector 对象(或 string 对象)本身是否是常量,返回值都是 const_iterator

  61. 箭头运算符把解引用和成员访问两个操作结合在一起,也就是说,it->mem(*it).mem 表达的意思相同。

  62. 虽然 vector 对象可以动态地增长,但是也会有一些副作用。已知的一个限制是不能在范围 for 循环中向 vector 对象添加元素。另外一个限制是任何一种可能改变 vector 对象容量的操作,比如 push_back,都会使该 vector 对象的迭代器失效。谨记,但凡是使用了迭代器的循环体,都不要向选代器所属的容器添加元素

  63. 假设用一个名为 text 的字符串向量存放文本文件中的数据,其中的元素或者是一句话或者是一个用于表示段落分隔的空字符串。下面的程序将 text 的第一段全都改成大写形式,然后进行输出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    vector<string> text(10);
    text[0] = "The world is fantastic.";
    text[1] = "If you have a dream, struggle for it.";

    for (auto iter = text.begin();
    iter != text.end() && !iter->empty();
    iter++)
    {
    for (auto &c : *iter)
    c = toupper(c);
    cout << *iter << endl;
    }

    编译代码后,运行可执行程序,将输出:

    1
    2
    THE WORLD IS FANTASTIC.
    IF YOU HAVE A DREAM, STRUGGLE FOR IT.
  64. vectorstring 迭代器支持的运算如下表所示:

    vector和string迭代器支持的运算
    iter + n 迭代器加上一个整数值仍得一个迭代器,迭代器指示的新位置与原来相比向前移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一位置
    iter - n 迭代器减去一个整数值仍得一个迭代器,迭代器指示的新位置与原来相比向后移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一位置
    iter1 += n 选代器加法的复合赋值语句,将iter1加n的结果赋给iter1
    iter1 -= n 迭代器减法的复合赋值语句,将iter1减n的结果赋给iter1
    iter1 - iter2 两个迭代器相减的结果是它们之间的距离,也就是说,将运算符右侧的迭代器向前移动差值个元素后将得到左侧的迭代器。参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一位置
    >、>=、<、<= 迭代器的关系运算符,如果某迭代器指向的容器位置在另一个迭代器所指位置之前,则说前者小于后者。参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一位置
  65. 只要两个迭代器指向的是同一个容器中的元素或者尾元素的下一位置,就能将其相减,所得结果是两个迭代器的距离。所谓距离指的是右侧的迭代器向前移动多少位置就能追上左侧的迭代器,其类型是名为 difference_type 的带符号整型数。stringvector 都定义了 difference_type,因为这个距离可正可负,所以 difference_type 是带符号类型的。

  66. 使用迭代器运算的一个经典算法是二分搜索:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 下面的程序使用迭代器完成了二分搜索
    // beg和end表示我们要搜索的范围
    auto beg = text.begin(), end = text.end();
    auto mid = beg + (end - beg) / 2;
    while (mid != end && *mid != sought)
    {
    if (*mid < sought)
    end = mid;
    else
    beg = mid + 1;
    mid = beg + (end - beg) / 2;
    }

    循环过程终止时,mid 或者等于 end 或者指向要找的元素。如果 mid 等于 end,说明 text 中没有我们要找的元素。

  67. 数组与 vector 相似的地方是,数组也是存放类型相同的对象的容器,这些对象本身没有名字,需要通过其所在位置访问。与 vector 不同的地方是,数组的大小确定不变,不能随意向数组中增加元素。因为数组的太小固定,因此对某些特殊的应用来说程序的运行时性能较好,但是相应地也损失了一些灵活性。如果不清楚元素的确切个数,请使用 vector

  68. 数组中元素的个数也属于数组类型的一部分,编译的时候维度应该是已知的。也就是说,维度必须是一个常量表达式

    1
    2
    3
    4
    5
    6
    unsigned cnt = 42;           // 不是常量表达式
    constexpr unsigned sz = 42; // 常量表达式
    int arr[10]; // 含有10个整数的数组
    int *parr[sz]; // 含有42个整型指针的数组
    string bad[cnt]; // 错误:cnt不是常量表达式
    string strs[get_size()]; // 当get_size是constexpr时正确;否则错误

    默认情况下,数组的元素被默认初始化。

  69. 定义数组的时候必须指定数组的类型,不允许用 auto 关键字由初始值的列表推断类型。另外和 vector 一样,数组的元素应为对象,因此不存在引用的数组。

  70. 可以对数组的元素进行列表初始化,此时允许忽略数组的维度。如果在声明时没有指明维度,编译器会根据初始值的数量计算并推测出来:相反,如果指明了维度,那么初始值的总数量不应该超出指定的大小。如果维度比提供的初始值数量大,则用提供的初始值初始化靠前的元素,剩下的元素被初始化成默认值

    1
    2
    3
    4
    5
    6
    const unsigned sz = 3;
    int ia1[sz] = {0, 1, 2}; // 含有3个元素的数组,元素值分别是0,1,2
    int a2[] = {0, 1, 2}; // 维度是3的数组
    int a3[5] = {0, 1, 2}; // 等价于a3[] = {0, 1, 2, 0, 0};
    string a4[3] = {"hi", "bye"}; // 等价于a4[] = {"hi", "bye", ""};
    int a5[2] = {0, 1, 2}; // 错误:初始值过多

    应注意,int a[10] = 1;并不会将数组 a 的所有元素初始化为 1。

  71. 字符数组有一种额外的初始化形式,可以用字符串字面值对此类数组初始化。当使用这种方式时,一定要注意字符串字面值的结尾处还有一个空字符,这个空字符也会像字符串的其他字符一样被拷贝到字符数组中去

    1
    2
    3
    4
    char a1[] = {'c', '+', '+'};         // 列表初始化,没有空字符
    char a1[] = {'c', '+', '+', '\0'}; // 列表初始化,含有显式的空字符
    char a3[] = "c++"; // 自动添加表示字符串结束的空字符
    const char a4[5] = "Hello"; // 错误:没有空间可存放空字符!

    关于最后一例,编译器会提示:

    数组空间不足,无法存放字符串字面值中的空字符

  72. 不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值

    1
    2
    3
    int a[] = {0, 1, 2};  // 含有3个整数的数组
    int a2[] = a; // 错误:不允许使用一个数组初始化另一个数组
    a2 = a; // 错误:不能把一个数组直接赋值给另一个数组

    一些编译器支持数组的赋值,这就是所谓的编译器扩展(compiler extension)。但一般来说,最好避免使用非标准特性,因为含有非标准特性的程序很可能在其他编译器上无法正常工作。

  73. 数组指针和指针数组总是容易让人混淆:

    1
    2
    3
    4
    int *ptrs[10];             // ptrs是含有10个整型指针的数组
    int &refs[10] = /* ? */; // 错误:不存在引用的数组
    int (*Parray)[10] = &arr; // Parry指向一个含有10个整数的数组
    int (&rref)[10] = arr; // arrRef引用一个含有10个整数的数组

    默认情况下,类型修饰符从右向左依次绑定。数组的维度是紧跟着被声明的名字的,所以就数组而言,由内向外阅读要比从右向左好多了。由内向外的顺序可帮助我们更好地理解 Parray 的含义。
    观察下面的代码片段:

    1
    int *(&arry)[10] = ptrs;

    按照由内向外的顺序阅读上述语句,首先知道 arry 是一个引用,然后观察右边知道,arry 引用的对象是一个大小为 10 的数组,最后观察左边知道,数组的元素类型是指向 int 的指针。这样,arry 就是一个含有 10 个 int 型指针的数组的引用。

    要想理解数组声明的含义,最好的办法是从数组的名字开始按照由内向外的顺序阅读。

  74. 在使用数组下标的时候,通常将其定义为 size_t 类型。size_t 是一种机器相关的无符号类型,它被设计得足够大以便能表示内存中任意对象的大小。cstddef 头文件中定义了 size_t 类型,这个文件是 C 标准库 stddef.h 头文件的 C++语言版本。

  75. vectorstring 一样,当需要遍历数组的所有元素时,最好的办法也是使用范围 for 语句。例如,下面的程序输出数组 scores 中的所有元素:

    1
    2
    3
    for (auto i : scores)  // 对于scores中的每个计数值
    cout << i << " "; // 输出当前的计数值
    cout << endl;

    因为维度是数组类型的一部分,所以系统知道数组 scores 中有多少个元素,使用范围for语句可以减轻人为控制遍历过程的负担。

    WARNING 大多数常见的安全问题都源于缓冲区溢出错误。当数组或其他类似数据结构的下标越界并试图访问非法内存区域时,就会产生此类错误。

  76. 像其他对象一样,对数组的元素使用取地址符就能得到指向该元素的指针:

    1
    2
    string nums[] = {"one", "two", "three"};  // 数组的元素是string对象
    string *p = &nums[0]; // p指向nums的第一个元素

    数组还有一个特性:在很多用到数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指针

    1
    string *p2 = nums;  // 等价于p2 = &nums[0];

    但应注意:&num&num[0] 拥有不同的含义。

    在大多数表达式中,使用数组类型的对象其实是使用一个指向该数组首元素的指针。

  77. 当使用数组作为一个 auto 变量的初始值时,推断得到的类型是指针而非数组

    1
    2
    3
    int ia[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};  // ia是一个含有10个整数的数组
    auto ia2(ia); // ia2是一个整型指针,指向ia的第一个元素
    ia2 = 42; // 错误:ia2是一个指针,不能用int值给指针赋值

    尽管 ia 是由 10 个整数构成的数组,但当使用 ia 作为初始值时,编译器实际执行的初始化过程类似于下面的形式:

    1
    auto ia2(&ia[0]);  // 显然 ia2 的类型是 int*

    当使用 decltype 关键字时上述转换不会发生decltype(ia) 返回的类型是由 10 个整数构成的数组:

    1
    2
    3
    4
    // ia3是一个含有10个整数的数组
    decltype(ia) ia3 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    ia3 = p; // 错误:不能用整型指针给数组赋值
    ia3[4] = i; // 正确:把i的值赋给ia3的一个元素
  78. 可以设法获取数组尾元素之后的那个并不存在的元素的地址:

    1
    2
    int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    int *e = &arr[10];
  79. C++ 新标准引入了两个名为 beginend 的函数,这两个函数与容器中的两个同名成员功能类似,不过数组不是类类型,因此两个函数不是成员函数:

    1
    2
    3
    int ia[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    int *beg = begin(ia); // 指向ia首元素的指针
    int *last = end(ia); // 指向ia尾元素的下一位置的指针

    begin 函数返回指向 ia 首元素的指针,end 函数返回指向 ia 尾元素下一位置的指针,这两个函数定义在 iterator 头文件中。

  80. 假设 arr 是一个整型数组,下面的程序负责找到 arr 中的第一个负数:

    1
    2
    3
    4
    // pbeg指向arr的首元素,pend指向arr尾元素的下一位置
    int *pbeg = begin(arr), *pend = end(arr);
    while (pbeg != pend && *pbeg >= 0)
    ++pbeg;

    一个指针如果指向了某种内置类型数组的尾元素的“下一位置”,则其具备与 vectorend 函数返回的与迭代器类似的功能。特别要注意,尾后指针不能执行解引用和递增操作。

  81. 给指针加上一个整数,得到的新指针仍需指向同一数组的其他元素,或者指向同一数组的尾元素的下一位置:

    1
    2
    3
    4
    5
    constexpr size_t sz = 5;
    int arr[sz] = {1, 2, 3, 4, 5};
    // 正确:arr转换成指向它首元素的指针;p指向arr尾元素的下一位置
    int *p = arr + sz; // 使用警告:不要解引用!
    int *p2 = arr + 10; // 错误:arr只有5个元素,p2的值未定义

    如果计算所得的指针超出了数组范围就将产生错误,而且这种错误编译器一般发现不了。

  82. 和迭代器一样,两个指针相减的结果是它们之间的距离。参与运算的两个指针必须指向同一个数组当中的元素

    1
    2
    int array[] = {1, 2, 3, 4, 5};
    auto size = std::end(array) - std::begin(array); // 以此获取数组中元素的数量

    两个指针相减的结果的类型是一种名为 ptrdiff_t 的标准库类型,和 size_t 一样,ptrdiff_t 也是一种定义在 cstddef 头文件中的机器相关的类型。因为差值可能为负值,所以 ptrdiff_t 是一种带符号类型。

  83. 只要两个指针指向同一个数组的元素,或者指向该数组的尾元素的下一位置,就能利用关系运算符对其进行比较:

    1
    2
    3
    4
    5
    6
    int *b = arr, *e = arr + sz;
    while (b < e)
    {
    // 使用*b
    ++b;
    }

    如果两个指针分别指向不相关的对象,则不能比较它们(没有意义)。

  84. 只要指针指向的是数组中的元素(或者数组中尾元素的下一位置),都可以执行下标运算:

    1
    2
    3
    int *p = &ia[2];  // p指向索引为2的元素
    int j = p[1]; // p[1]等价于*(p + 1),即ia[3]表示的那个元素
    int k = p[-2]; // p[-2]是ia[0]表示的那个元素

    标准库类型限定使用的下标必须是无符号类型,而内置的下标运算无此要求,当然,结果地址必须指向原来的指针所指同一数组中的元素(或是同一数组尾元素的下一位置)。

    内置的下标运算符所用的索引值不是无符号类型,这一点与 vectorstring 不一样。

  85. 假定 p1 和 p2 指向同一个数组中的元素,则下面程序的功能是什么?什么情况下该程序是非法的?

    1
    p1 += p2 -p1;

    使 p1 指向 p2 所指元素。当 p1 为常量指针时(即 p1 本身的值不可更改时),该程序非法。

  86. C 语言标准库提供了一组函数,如下表所示。这些函数可用于操作 C 风格字符串,它们定义在 cstring 头文件中。

    C风格字符串的函数
    strlen(p) 返回p的长度,空字符不计算在内
    strcmp(p1, p2) 比较p1和p2的相等性。如果p1 == p2,返回0;如果p1 > p2,返回一个正值;如果p1 < p2,返回一个负值
    strcat(p1, p2) 将p2附加到p1之后,返回p1
    strcpy(p1, p2) 将p2拷贝给p1,返回p1

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

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

    但实际调试结果是,上述代码可以正常输出结果 3。

  87. 观察下述代码片段:

    1
    2
    3
    4
    5
    6
    const char ca1[] = "A string example";
    const char ca2[] = "A different string";
    if (ca1 < ca2) // 未定义的:试图比较两个无关的地址
    {
    // do something
    }

    当使用数组的时候其实真正用的是指向数组首元素的指针,上面的 if 条件实际上比较的是两个 const char* 的值。

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

  88. 下面程序的输出结果是什么?

    1
    2
    3
    4
    5
    6
    7
    const char ca1[] = {'h', 'e', 'l', 'l', 'o'};
    const char *cp = ca1;
    while (*cp)
    {
    cout << *cp << endl;
    ++cp;
    }

    逐行输出 ca1 中的内容,但*cp 什么时候为假?ca1 尾元素的下一位置是空字符‘\0’,这是编译器对字符数组的默认处理吗?

  89. 此前已介绍过允许使用字符串字面值来初始化 string对象:

    1
    string s("Hello World"); // s 的内容是 Hello World

    更一般的情况是,任何出现字符串字面值的地方都可以用以空字符结束的字符数组来替代

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

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

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

    c_str 函数的返回值是一个 C 风格的字符串。也就是说,c_str 函数的返回结果是一个指针,该指针指向一个以空字符结束的字符数组,而这个数组所存的数据恰好与那个 string 对象的一样。结果指针的类型是 const char*,从而确保我们不会改变字符数组的内容。无法保证c_str 函数返回的数组一直有效,事实上,如果后续的操作改变了 s 的值就可能让之前返回的数组失去效用。

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

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

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

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

    1
    2
    // 拷贝三个元素: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 风格的基于数组的字符串。

    不敢苟同,看需求,如果仅需要存储一些数据,那数组足够。(博主注)

  92. 当一个数组的元素仍然是数组时,通常使用两个维度来定义它:一个维度表示数组本身大小,另外一个维度表示其元素(也是数组)大小。

  93. 允许使用花括号括起来的一组值初始化多维数组:

    1
    2
    3
    4
    5
    6
    int ia[3][4] =
    {
    {0, 1, 2, 3}, // 第1行的初始值
    {4, 5, 6, 7}, // 第2行的初始值
    {8, 9, 10, 11} // 第3行的初始值
    }

    其中,内层嵌套着的花括号并非必须:

    1
    int ia[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};

    类似于一维数组,在初始化多维数组时也并非所有元素的值都必须包含在初始化列表之内。如果仅仅想初始化每一行的第一个元素,通过如下的语句即可:

    1
    int ia[3][4] = {{0}, {4}, {8}};

    其他未列出的元素执行默认值初始化,这个过程和一维数组一样。在这种情况下如果再省略掉内层的花括号,结果就大不一样了:

    1
    int ix[3][4] = {0, 3, 6, 9};  // 显式地初始化第1行,其它元素执行值初始化
  94. 如果表达式含有的下标运算符数量比数组的维度小,则表达式的结果将是给定索引处的一个内层数组:

    1
    int (&row)[4] = ia[1];  // 把row定义成一个含有4个整数的数组的引用,然后绑定到ia的第二个4元素数组上
  95. 使用范围 for 语句处理二维数组中的每一个元素:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    constexpr size_t rowCnt = 3, colCnt = 4;
    size_t cnt = 0;
    int ia[rowCnt][colCnt];
    for (auto &row : ia)
    {
    for (auto &col : row)
    {
    col = cnt;
    ++cnt;
    }
    }

    在上面的例子中,因为要改变数组元素的值,所以我们选用引用类型作为循环控制变量,但其实还有一个深层次的原因促使我们这么做。举一个例子,考虑如下的循环:

    1
    2
    3
    for (const auto &row : ia)  // 对于外层数组的每一个元素
    for (auto col : row) // 对于内层数组的每一个元素
    cout << col << endl;

    这个循环中并没有任何写操作,可是我们还是将外层循环的控制变量声明成了引用类型,这是为了避免数组被自动转成指针。假设不用引用类型,则循环如下述形式:

    1
    2
    for (const auto row : ia)  // 对于外层数组的每一个元素
    for (auto col : row) // 对于内层数组的每一个元素

    程序将无法通过编译。这是因为,像之前一样第一个循环遍历 ia 的所有元素,注意这些元素实际上是大小为 4 的数组。因为 row 不是引用类型,所以编译器初始化 row 时会自动将这些数组形式的元素(和其他类型的数组一样)转换成指向该数组内首元素的指针。这样得到的 row 的类型就是 int*,显然内层的循环就不合法了,编译器将试图在一个 int* 内遍历,这显然和程序的初衷相去甚远。

    要使用范围 for 语句处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型。

  96. 当程序使用多维数组的名字时,也会自动将其转换成指向数组首元素的指针。因为多维数组实际上是数组的数组,所以由多维数组名转换得来的指针实际上是指向第一个内层数组的指针:

    1
    2
    3
    int ia[3][4];
    int (*p)[4] = ia; // p指向含有4个整数的数组
    p = &ia[2]; // p指向ia的尾元素
  97. 通过使用 autodecltype 关键字,可以尽可能地避免在数组前加上一个指针类型:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 输出ia中每个最小元素的值,每个内层数组各占一行
    // p指向含有4个整数的数组
    for (auto p = ia; p != ia + 3; p++)
    {
    // q指向4个整数数组的首元素,意即,q指向一个整数
    for (auto q = *p; q != *p +4; ++q)
    cout << *q << ' ';
    cout << endl;
    }

    使用标准库函数 beginend 也能实现同样的功能,而且更加简洁:

    1
    2
    3
    4
    5
    6
    7
    8
    // p指向ia的第一个数组
    for (auto p = begin(ia); p != end(ia); ++p)
    {
    // q指向内层数组的首元素
    for (auto q = begin(*p); q != end(*p); ++q)
    cout << *q << ' '; // 输出q所指的整数值
    cout << endl;
    }
  98. 使用数组别名简化工作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    using int_array = int[4];  // 使用using声明的方式
    typedef int int_array[4]; // 使用typedef声明的方式

    // 输出ia中每个最小元素的值,每个内层数组各占一行
    for (int_array *p = ia; p != ia + 3; ++p)
    {
    for (int *q = *p; q != *p + 4; ++q)
    cout << *q << ' ';
    cout << endl;
    }
  99. 只有当逻辑与运算符 && 左侧运算对象为真时才会检查其右侧运算对象的真值,只有当逻辑或运算符 || 左侧运算对象为假时才会检查其右侧运算对象的真值。


Thank you for your donate!