第3章 字符串、向量和数组

  • 第3章 字符串、向量和数组
    • 3.1 命名空间的 using 声明
      • 头文件不应包含 using 声明
    • 3.2 标准库类型 string
      • 3.2.1 定义和初始化string对象
        • 直接初始化和拷贝初始化
      • 3.2.2 string 对象上的操作
        • 使用getline读取一整行
        • string::size_type类型
        • 比较 string 对象
        • 字面值和 string 对象相加
      • 3.2.3 处理 string 对象中的字符
        • 处理每个字符?使用基于范围的for语句
        • 使用范围 for 语句改变字符串中的字符
        • 只处理一部分字符?
        • 使用下标执行迭代
        • 使用下标执行随机访问
    • 3.3 标准库类型 vector
      • 3.3.1 定义和初始化 vector 对象
        • 列表初始化vector对象 P88
        • 创建指定数量的元素
        • 值初始化
        • 列表初始值还是元素数量?
      • 3.3.2 向vector对象中添加元素
        • vector对象添加元素蕴含的编程假定
      • 3.3.3 其他vector操作
        • 计算vector内对象的索引
        • 不能用下标形式添加元素
    • 3.4 迭代器介绍
      • 3.4.1 使用迭代器
        • 迭代器运算符
        • 将迭代器从一个元素移动到另外一个元素
        • 迭代器类型
        • beginend运算符
        • 结合解引用和成员访问操作
        • 某些对vector对象的操作会使迭代器失效
      • 3.4.2 迭代器运算 (iterator arithmetic)
        • 迭代器的算术运算
        • 使用迭代器运算
    • 3.5 数组
      • 3.5.1 定义和初始化内置数组
        • 显式初始化数组元素
        • 字符数组的特殊性
        • 不允许拷贝和赋值
        • 理解复杂的数组声明
      • 3.5.2 访问数组元素
        • 检查下标的值
      • 3.5.3 指针和数组
        • 指针也是迭代器
        • 标准库函数beginend
        • 指针运算
        • 解引用和指针运算的交互
        • 下标和指针
      • 3.5.4 C风格字符串
        • C 标准库 String 函数
        • 比较字符串
        • 目标字符串的大小由调用者指定
      • 3.5.5 与旧代码的接口
        • 混用 string 对象和 C风格字符串
        • 使用数组初始化 vector 对象
    • 3.6 多维数组
      • 多维数组的初始化
      • 多维数组的下表引用
      • 使用范围 for 语句处理多维数组
      • 指针和多维数组
      • 类型别名简化多维数组的指针

第3章 字符串、向量和数组

stringvector是两种最重要的标准库类型。string支持可变长字符串,后者则表示可变长的集合。
迭代器,是 string 和 vector 的配套类型,常被用于访问 string 中的字符或 vector 中的元素。

3.1 命名空间的 using 声明

using声明(using declaration)
std::cin表示从标准输入中读取内容,::为作用域操作符。
含义:编译器应从操作符左侧名字所示的作用域中寻找右侧那个名字。std::cin的意思就是要使用命名空间std中的名字cin

头文件不应包含 using 声明

原因:因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个 using 声明,那么每个使用了该头文件的文件就都会有这个声明。对于某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突。

3.2 标准库类型 string

标准库类型string表示可变长的字符序列。

3.2.1 定义和初始化string对象

表3.1: 初始化string对象的方式
string s1 默认初始化,s1 是一个空串
string s2(s1) s2s1 的副本
string s2 = s1 等价于 s2(s1)s2s1 的副本
string s3("value”) s3 是字面值"value"的副本,除了字面值最后的那个空字符外
string s3 = "value" 等价于 s3("value)s3 是字面值"value"的副本
string s4(n, 'c') s4 初始化为由连续 n 个字符 c 组成的串

直接初始化和拷贝初始化

  • 拷贝初始化(copy initialization),编译器把等号右侧的初始值拷贝到新创建的对象中去
  • 直接初始化(direct initialization),不使用等号

string s5 = "hiya"; // 拷贝初始化
string s6("hiya"); // 直接初始化
string s7(10, 'c'); // 直接初始化,s7 的内容是 ccccccc √ 可读性更好
string s8 = string(10, 'c'); // 拷贝初始化,s8 的内容是 cccccccc

注:s8 的拷贝初始化本质上等价于下面的两条语句:
string temp(10, ‘c’); // temp 的内容是 cccccccc
string s8 = temp; // 将 temp 拷贝给 s8

3.2.2 string 对象上的操作

表3.2 string的操作
os< 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个字符的引用,位置n0计起
s1+s2 返回s1s2连接后的结果
s1 = s2 s2的副本代替s1中原来的字符
s1 == s2 如果s1s2中所含的字符完全一样,则它们相等;
s1 != s2 string对象的相等性判断对字母的大小写敏感
<, <=, >, >= 利用字符在字典中的顺序进行比较,且对字母的大小写敏感

在执行读取操作时,string对象会自动忽略开头的空白(即空格符、换行符、制表符等)并从第一个真正的自负开始读起,直到遇见下一处空白为止。如果程序的输入是"Hello World!",则输出将是“Hello”。

/* 如果程序输入的内容为“     Hello World!     ” */

/* 情况1:只能输出 Hello 的情况:*/
int main()
{
     
    string s;   // 空字符串
    cin >> s;   // 将 string 对象读入 s,遇到空白停止
    cout << s << endl;  // 输出 s
    return 0;
}

/* 情况2:输出 HelloWorld! 的情况: */
int main()
{
     
    string s1, s2;
    cin >> s1 >> s2;    // 把第一个输入读到 s1 中,第二个输入读到 s2 中
    cout << s1 << s2 << endl;   // 输出两个 string 对象
}

使用getline读取一整行

使用情景:有时我们希望能在最终得到的字符串中保留输入时的空白符,这时应该用 getline 函数代替原来的 >> 运算符。

getline与换行符水火不容
getline函数的参数是一个输入流和一个string对象,
函数从给定的输入流中读入内容,直到遇到换行符为止(注意换行符也被读进来了),
然后把所读的内容存入到那个string对象中去(注意不存换行符)。
Note:触发getline函数返回的那个换行符实际上被丢掉了,得到的string对象中并不包含该换行符。
getline只要一遇到换行符就结束读取操作并返回结果,哪怕输入的一开始就是换行符也是如此,此种情况下那么所得的结果是个空string。

我们也能用getline的结果作为判断条件,可以让程序一次输出一整行,而不再是每行输出一个词了:

int main()
{
     
    string line;
    // 每次读入一整行,直至到达文件末尾
    while (getline(cin, line))
        cout << line << endl;
    return 0;
}

因为line中不包含换行符,所以我们手动地加上换行操作符。使用 endl结束当前行并刷新显示缓冲区。

练习3.2:编写一段程序从标准输入中一次读入一整行,然后修改该程序使其一次读入一个词。
输入一整行:while (getline(cin, line))
输入一个词:while (cin >> word)

/* 练习3.2:编写一段程序从标准输入中一次读入一整行,然后修改该程序使其一次读入一个词。

【思路】:常用的字符串读取方式有两种:
(1) 使用`getline`函数一次读入一整行;
行的结束标识是回车符,如果一开始输入的就是回车符,则`getline`直接结束本次读取,所得的结果是一个空字符串
while (getline(cin, line))

(2) 使用`cin`一次读入一个词,遇空白停止。
while (cin >> word)
*/

// (1) 使用 getline 一次读入一整行,遇回车结束
#include 
#include 

using namespace std;

int main()
{
     
    string line;
    cout << "Please enter a string including space key: " << endl;
    while (getline(cin, line))
        cout << line << endl;
    return 0;
}

// (2) 使用 cin 一次读入一个词,遇空白结束
#include 
#include 

using namespace std;

int main()
{
     
    string word;
    cout << "Please enter a word, without space: " << endl;
    while (cin >> word)
        cout << word << endl;
    return 0;
}

练习 3.3:请说明 string 类的输入运算符和 getline 函数分别是如何处理空白字符的。

  • 标准库 string 的输入运算符自动忽略字符串开头的空白(包括空格符、换行符、制表符等),从第一个真正的字符开始读起,直到遇见下一处空白为止。
  • 如果希望在最终的字符串中保留输入时的空白符,应该使用 getline 函数代替原来的 >>运算符,getline从给定的输入流中读取数据,直到遇到换行符为止,此时换行符也被读取进来,但是并不存储在最后的字符串中。

string::size_type类型

string::size_type是一个无符号类型的值,而且能足够存放下任何 string 对象的大小。

注意:在表达式中混用了带符号数和无符号数将可能产生意想不到的结果。
如果一条表达式中已经有了size()函数就不要再使用int了,这样可以避免混用intunsigned可能带来的问题。

举例 P93:s.size 函数返回值的类型是 string::size_type

/* 使用范围 for 语句和 ispunct 函数来统计 string 对象中标点符号的个数:*/
string s("Hello Wolrd!!!");
// punct_cnt 的类型和 s.size 的返回类型一样
decltype(s.size()) punct_cnt = 0;
// 统计 s 中标点符号的数量
for (auto c : s)        // 对于 s 中的每个字符
    if (ispunct(c))     // 如果该字符是标点符号
        ++punct_cnt;    // 将标点符号的计数值加 1
cout << punct_cnt << " punctuation characters in " << s << endl;

比较 string 对象

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

示例:

string str = “Hello”;
string phrase = “Hello World”;
string slang = “Hiya”;

判断:对象 str 小于对象 phrase;对象 slang 既大于 str 也大于 phrase。

字面值和 string 对象相加

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

string s4 = s1 + ", ";      // 正确:把一个 string 对象和一个字面值相加
string s5 = "hello" + ", "; // 错误:两个运算对象都不是 string
string s6 = s1 + ", " + "world";    // 正确:每个加法运算符都有一个运算对象是 string
        // s6 的初始化形式工作机理和连续输入连续输出是一样的,可以用如下的形式分组:
        //    string s6 = (s1 + ", ") + "world";
string s7 = "hello" + ", " + s2;    // 错误:不能把字面值直接相加

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

3.2.3 处理 string 对象中的字符

表3.3 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

建议:使用 C++ 版本的 C 标准库头文件。比如cctype头文件和ctype.h头文件的内容是一样的,只不过从命名规范上来讲更符合 C++ 语言的要求。

处理每个字符?使用基于范围的for语句

范围for(range for)语句,这种语句遍历给定序列中的每个元素并对序列中的每个值执行某种操作:
expression 表示一个序列,declaration 定义一个变量,该变量将被用于访问序列中的基础元素。
每次迭代,declaration 部分的变量会被初始化为 expression 部分的下一个元素。

for (declaration : expression)
    statement

举例:使用范围 for 语句把 string 对象中的字符每行一个输出出来:

string str("some string");
// 每行输出 str 中的一个字符
for (auto c : str)      // 对于 str 中的 每个字符
    cout << c << endl;  // 输出当前字符,后面紧跟着一个换行符

使用范围 for 语句改变字符串中的字符

如果想要改变 string 对象中字符的值,必须把循环变量定义成引用类型。

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

练习3.8:分别用 while 循环和传统的 for 循环重写 范围for循环 的程序。

/* 练习3.8:分别用 while 循环和传统的 for 循环重写 范围for循环 的程序。
你觉得哪种形式更好呢?为什么? */

/*--------------------------*/

// 使用 范围for循环
#include 
#include 

using namespace std;

int main()
{
     
    string s;
    cout << "Please enter a string, including space: " << endl;
    getline(cin, s);    // 读取整行,遇回车符结束
    for (auto &c : s)   // 依次处理字符串中的每一个字符
    {
     
        c = 'X';
    }
    cout << s << endl;
    return 0;
}

/*--------------------------*/

// 使用传统 for 循环实现的程序如下:
#include 
#include 

using namespace std;

int main()
{
     
    string s;
    cout << "Please enter a string, including space: " << endl;
    getline(cin, s);
    for (unsigned int i = 0; i < s.size(); i++)   // 使用 unsigned int i
    {
     
        s[i] = 'X';
    }
    cout << s << endl;
    return 0;
}

/*--------------------------*/

// 使用 while 循环实现的程序如下:
#include 
#include 

using namespace std;

int main()
{
     
    string s;
    cout << "Please enter a string, including space: " << endl;
    getline(cin, s);
    int i = 0;
    while(s[i] != '\0') // 编译器在每个字符串的结尾处添加一个空字符(`'\0'`)
    {
     
        s[i] = 'X';
        ++i;
    }
    cout << s << endl;
    return 0;
}

/*在本例中,我们希望处理字符串中的每一个字符,且无需在意字符的处理顺序,
因此与传统的 while 循环和 for 循环相比,使用范围 for 循环更简洁直观。*/

练习3.11:下面的范围for语句合法吗?如果合法,c的类型是什么?

/* 练习3.11:下面的范围for语句合法吗?如果合法,c的类型是什么?*/
const string s = "Keep out!";
for (auto &c : s)
{
     
    c = 'X';
}

解答:语法上来说是合法的,s 是一个常量字符串,则c的推断类型是常量引用,即c所绑定的对象值不能改变。
由于c是绑定到常量的引用,其值不能改变。否则编译器会报错。

只处理一部分字符?

访问 string 对象中的单个字符有两种方式:

    1. 使用下标
    1. 使用迭代器 (将在3.4节P95 和第9章中介绍)

下标运算符([]),接受的输入参数是 string::size_type类型的值,这个参数表示要访问的字符的位置;返回值是该位置上字符的引用。例:

/*将字符串的首字符改成大写形式*/
string s("some string");
if (!s.empty())             // 确保 s[0] 的位置确实有字符
    s[0] = toupper(s[0]);   // 为 s 的第一个字符赋一个新值

练习3.10:编写一段程序,读入一个包含标点符号的字符串,将标点符号去除后输出字符串剩余的部分。

/* 练习3.10:编写一段程序,读入一个包含标点符号的字符串,将标点符号去除后输出字符串剩余的部分。*/

// 思路一:利用范围for语句遍历字符串,逐个输出非标点字符:

#include 
#include 
#include 

using namespace std;

int main()
{
     
    string s;
    cout << "Please enter a string including punctuations: " << endl;
    getline(cin, s);
    for (auto c : s)
    {
     
        if (!ispunct(c))
            cout << c;
    }
    cout << endl;
    return 0;
}


// 思路二:利用普通 for 循环遍历字符串,通过下标执行随机访问,把非标点字符拼接成一个新串后输出:

#include 
#include 
#include 

using namespace std;

int main()
{
     
    string s, result;
    cout << "Please enter a string including punctuations: " << endl;
    for (decltype(s.size()) i = 0; i < s.size(); i++)
    {
     
        if (!ispunct(s[i])) // 下标运算符
            result += s[i];
    }
    cout << result << endl;
    return 0;
}

使用下标执行迭代

for循环使用变量index作为s的下标,index的类型是由decltype关键字决定的。

/* 依次处理 s 中的字符直至我们处理完全部字符或者遇到一个空白 */
for (decltype(s.size()) index = 0;
    index != s.size() && !isspace(s[index]);
    ++index)
    s[index] = toupper(s[index]);   // 将当前字符改成大写形式

提示:注意检查下标的合法性:下标必须大于等于 0 而小于字符串的 size() 的值。一种简便易行的方法是,总是设下标的类型为 string::size_type,因此此类型是无符号数,可以确保下标不会小于 0。此时,代码只需保证下标小于 size() 的值就可以了。

使用下标执行随机访问

/* 编写一个程序把 0 到 15 之间的十进制数转换成对应的十六进制形式,
只需初始化一个字符串令其存放 16 个十六进制“数字” */
const string hexdigits = "0123456789ABCDEF"; // 可能的十六进制数字
cout << "Enter a series of numbers between 0 and 15 separated by spaces."
     << "Hit ENTER when finsihed: "
     << endl;
string result;          // 用于保存十六进制的字符串
string::size_type n;    // 用于保存从输入流读取的数
while (cin >> n)
    if (n < hexdigits.size())   // 忽略无效输入确保输入的数小于16
        result += hexdigits[n]; // 得到对应的十六进制数字
cout << "Your hex number is: " << result << endl;

hexdigits[n]的值就是 hexdigits 内位置n处的字符。例如,如果 n 是 15,则结果是 F;如果 n 是 12,则结果是 C

下标nstring::size_type类型,也就是无符号类型,所以n可以确保大于或等于 0。

3.3 标准库类型 vector

标准库类型vector表示对象的几何,其中所有对象的类型都相同。因为vector“容纳着”其他对象,所以它也被称作容器(container)。

C++语言既有类模板(class template),也有函数模板,其中vector是一个类模板。

模板本身不是类或函数,相反可以将模板看作为编译器生成类或函数编写的一分说明。编译器根据模板创建类或函数的过程称为实例化(instantiation),当使用模板时,需要指出编译器应把类或模板实例化成何种类型。

对于类模板来说,我们通过提供一些额外信息来指定模板到底实例化成什么样的类,需要提供哪些信息由模板决定。提供信息的方式:在模板名字后面跟一对尖括号,在括号内放上信息。

vector<int> ivec;               // ivec 保存 int 类型的对象
vector<Sales_item> Sales_vec;   // 保存 Sales_item 类型的对象
vector<vector<string>> file;    // 该向量的元素是 vector 对象

3.3.1 定义和初始化 vector 对象

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

注意两个 vector 对象的类型必须相同:

vector<int> ivec;           // 初始状态为空
vector<int> ivec2(ivec);    // 把 ivec 的元素拷贝给 ivec2
vector<int> ivec3 = ivec;   // 把 ivec 的元素拷贝给 ivec3
vector<string> svec(ivec2); // 错误:svec的元素是string对象,不是int

练习3.12:下列 vector 对象的定义有不正确的吗?

/* 练习3.12:下列 vector 对象的定义有不正确的吗?
如果有,请指出来。对于正确的,描述其执行结果;对于不正确的,说明其错误的原因。*/

(a) vector<vector<int>> ivec;
// 正确。定义了一个名为 ivec 的 vector 对象,其中的每个元素都是 vector 对象。

(b) vector<string> svec = ivec;
// 错误。svec 的元素类型是 string,而 ivec 的元素类型是 int,因此不能使用 ivec 初始化 svec。

(c) vector<string> svec(10, "null");
// 正确。定义了一个名为 svec 的 vector 对象,其中含有 10 个元素,每个元素都是字符串 null。(null 是一个 int 变量,但这里写的是 "null",是字符串)。

列表初始化vector对象 P88

列表初始化:用花括号括起来 0 个或多个厨师元素值被赋给 vector 对象:

vector articles = {"a", "an", "the"};

回顾:C++定义了初始化的几种不同形式:P39
其中用花括号来初始化变量的这种初始化形式被称为列表初始化(list initialization)。

int units_sold = 0;
int units_sold = {
     0};
int units_sold{
     0};
int units_sold(0);

列表初始化 vector 对象,特殊要求是:如果提供的是初始元素值的列表,则只能把初始值都放在花括号里进行列表初始化,而不能放在圆括号里:

vector<string> v1{
     "a", "an", "the"};    // 列表初始化
vector<string> v2("a", "an", "the");    // 错误

创建指定数量的元素

还可以用vector对象容纳的元素数量和所有元素的统一初始值来初始化 vector 对象:

vector<int> ivec(10, -1);       // 10 个 int 类型的元素,每个都被初始化为 -1
vector<string> svec(10, "hi!"); // 10 个 string类型的元素,每个都被初始化为 "hi!"

值初始化

通常情况下,可以只提供 vector 对象容量的元素数量而不用略去初始值。此时库会创建一个值初始化的(value-initialized)元素初值,并把它赋给容器中的所有元素。这个初值由 vector 对象中元素的类型决定。

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

列表初始值还是元素数量?

花括号 vs 圆括号:

 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{
     10, 1};     // v4 有 2 个元素,值分别是 10 和 1

 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 是列表初始化。要想列表初始化对象,花括号里的值必须与元素类型相同。

练习3.13:下列的 vector 对象各包含多少个元素怒?这些元素的值分别是多少?

/* 练习3.13:下列的 vector 对象各包含多少个元素怒?这些元素的值分别是多少? */

(a) vector<int> v1;
// 元素数量为 0

(b) vector<int> v2(10);
// 10 个元素,每个元素都被初始化为 0

(c) vector<int> v3(10, 42);
// 10 个元素,每个元素都被初始化为 42

(d) vector<int> v4{
     10};
// 1 个元素,元素值为 10

(e) vector<int> v5{
     10, 42};
// 2 个元素,元素值分别为 10 和 42

(f) vector<string> v6{
     10};
// 10 个元素,每一个元素都被初始化为空字符串

(g) vector<string> v7{
     10, "hi"};
// 10 个元素,每一个元素都被诶初始化为 "hi"
/* 练习3.19:如果想定义一个含有 10 个元素的 vector 对象,所有元素的值都是 42,请列举出三种不同的实现方法。哪种方法更好呢?为什么?*/

// 思路一:先定一个空 vector 对象,然后添加元素。
vector<int> vInt;
for (int i = 0; i < 10; i++)
    vInt.push_back(42);

// 思路二:列表初始化,罗列出全部 10 个元素的值。
vector<int> vInt = {
     42,42,42,42,42,42,42,42,42,42};

// 思路三:用括号给出所有元素的值,效果类似于思路二。
vector<int> vInt{
     42,42,42,42,42,42,42,42,42,42};

// 思路四:定义的时候使用参数指定元素个数及重复的值。(这个最好)
vector<int> vInt(10, 42);

// 思路五:先指定元素个数,再利用范围for循环依次为元素赋值。
vector<int> vInt(10);
for (auto &i : vInt)
    i = 42;

// 思路四采用的初始化方式形式上最简洁直观。

3.3.2 向vector对象中添加元素

创建一个空vector,运行时再利用 push_back向其中添加元素,push_back负责把一个值当成vector对象的尾元素“压到(push)” vector对象的“尾端(back)”。

vector<int> v2;         // 空 vector 对象
for (int i = 0; i != 100; ++i)
    v2.push_back(i);    // 依次把整数值放到 v2 尾端
// 循环结束后 v2 有 100 个元素,值从 0 到 99
/* 如果直到运行时才能直到 vector 对象中元素的确切个数,也应使用这种方法
比如:从标准输入中读取单词,将其作为 vector 对象的元素存储 */

string word;
vector<string> text;        // 空 vector 对象
while (cin >> word) {
     
    text.push_back(word);   // 把 word 添加到 text 后面
}

关键概念:vector对象能高效增长
当所有(all)元素的值都一样时,在定义 vector 对象的时候有必要设定其大小;
一旦元素的值有所不同,更有效的办法是先定一个空的 vector 对象,再在运行时向其中添加具体值。
这跟 C 或 Java在创建对象时顺便制定其容量的做法恰恰相反。

vector对象添加元素蕴含的编程假定

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

练习3.14:编写一段程序,用 cin 读入一组整数并把它们存入一个 vector 对象。

/* 练习3.14:编写一段程序,用 cin 读入一组整数并把它们存入一个 vector 对象。*/

#include 
#include 

using namespace std;

int main()
{
     
    vector<int> vInt;       // 元素类型为 int 的 vector 对象
    int i;                  // 记录用户的输入值
    char cont = 'y';        // 与用户交互,决定是否继续输入
    while (cin >> i)
    {
     
        vInt.push_back(i);  // 向 vector 对象中添加元素
        cout << "Do you wanna continue? (y or n)? " << endl;
        cin >> cont;
        if (cont != 'y' && cont != 'Y')
            break;
    }
    for (auto mem : vInt)   // 使用范围for循环语句遍历 vInt 中的每个元素
        cout << mem << " ";
    cout << endl;
    return 0;
}
/* Output:
1
Do you wanna continue? (y or n)?
y
2
Do you wanna continue? (y or n)?
y
3
Do you wanna continue? (y or n)?
Y
4
Do you wanna continue? (y or n)?
1 2 3 4
*/
/* 练习 3.17:从 cin 读入一组词并把它们存入一个 vector 对象,
然后设法把所有词都改写为大写形式。
输出改变后的结果,每个词占一行。*/

#include 
#include 
#include 

using namespace std;

int main()
{
     
    vector<string> vString;
    string word;
    char cont = 'y';
    cout << "Enter the first word: " << endl;
    while (cin >> word)
    {
     
        vString.push_back(word);
        cout << "Do you wanna continue (y or n)? " << endl;
        cin >> cont;
        if (cont != 'y' && cont != 'Y')
            break;
        cout << "Please enter the next word: " << endl;
    }
    cout << "The converted result is: " << endl;
    for (auto &mem : vString) {
      // 使用范围for循环语句遍历vString中的每个元素
        for (auto &c : mem)     // 使用范围for循环语句遍历mem中的每个字符
            c = toupper(c);     // 改写为大写字母形式
        cout << mem << endl;
    }
    return 0;
}

/* Output:
Enter the first word: 
Meow
Do you wanna continue (y or n)? 
y
Please enter the next word: 
Alex
Do you wanna continue (y or n)? 
Y
Please enter the next word: 
Bordora
Do you wanna continue (y or n)? 
n
The converted result is:
MEOW
ALEX
BORDORA
*/

3.3.3 其他vector操作

表3.5: 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 v1v2相等当且晋档它们的元素数量相同且对应位置的元素值都相同
v1 != v2
<, <=, >, >= 顾名思义,以字典顺序进行比较

使用范围for语句处理 vector 对象中的所有元素:

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;

第一个循环把控制变量 i 定义成引用类型,这样就能通过 i 给 v 的元素赋值,其中 i 的类型由 auto 关键字指定。
size返回 vector 对象中元素的个数,返回值的类型是由 vector 定义的size_type类型。
要使用size_type,需首先指定它是由哪种类型定义的。vector对象的类型总是包含着元素的类型:

vector<int>::size_type  // 正确
vector::size_type       // 错误

计算vector内对象的索引

下标的类型也是相应的size_type类型。

举个例子,假设有一组成绩的集合,其中成绩的取值是从 0 到 100。以 10 分为一个分数段,要求统计各个分数段各有多少个成绩。
显然,从 0 到 100 总共有 101 种可能的成绩取值,这些成绩分布在 11 个分数段上:每 10 个分数构成一个分数段,这样的分数段有 10 个,额外还有一个分数段表示满分 100 分。
这样第一个分数段将统计成绩在 0 到 9 之间的数量;
第二个分数段将统计成绩在 10 到 19 之间的数量,以此类推。
最后一个分数段统计满分 100 分的数量。
按照上面的描述,如果输入的成绩如下:
42 65 95 100 39 67 95 76 88 76 83 92 76 93
则输出的结果应该是:
0 0 0 1 1 0 2 3 2 4 1

结果显示:
成绩在30分以下的没有、
30分至39分有 1 个、
40分至49分有 1 个、
50分至59分没有、
60分至69分有 2 个、
70分至79分有 3 个、
80分至89分有 2 个、
90分至99分有 4 个、
还有 1 个是满分。

/* 以 10 分为一个分数段统计成绩的数量:0~9, 10~19, ..., 90~99, 100 */

vector<unsigned> scores(11, 0); // 11 个分数段,全都初始化为 0
unsigned grade;
while (cin >> grade) {
               // 读取成绩
    if (grade <= 100)           // 检查读入的成绩是否合法(即是否小于等于100分)
        ++scores[grade/10];     // 将成绩对应分数段的计数值加 1
}

执行计数值累加的那条语句很好地体现了 C++ 程序代码的简洁性。
++scores[grade/10]; // 将当前分数段的计数加 1

等价于:
auto ind = grade/10; // 得到分数段索引
score[ind] = score[ind] + 1; // 将计数值加 1

另一种方法:用迭代器(iterator)改写该程序并实现完全相同的功能。(练习 3.25)

#include 
#include 

using namespace std;

int main()
{
     
    vector<unsigned> scores(11);
    auto it = scores.begin();
    int grade;
    cout << "Please enter a group of scores (0~100): " << endl;
    while (cin >> grade)
        if (grade <= 100)
            ++*(it + grade/10); //利用迭代器定位到对应的元素,加 1
    cout << "You have inputted in total " << scores.size() << " scores." << endl;
    cout << "The number of people in each band (from low to high) is: " << endl;
    // 利用迭代器遍历 scores 的元素并逐个输出
    for (it = scores.begin(); it != scores.end(); it++)
    {
     
        cout << *it << " ";
    }
    cout << endl;

    return 0;
}

不能用下标形式添加元素

/* 通过 vector 对象的下标形式来添加元素可能会产生错误 */
vector<int> ivec;
for (decltype(ivec.size()) ix = 0; ix != 10; ++ix)
    ivec[ix] = ix;  // 严重错误:ivec 不包含任何元素

ivec是一个空vector,根本不包含任何元素,当然也就不能通过下标去访问任何元素。正确的方法是使用push_back

vector<int> ivec;
for (decltype(ivec.size()) ix = 0; ix != 10; ++ix)
    ivec.push_back(ix); // 正确:添加一个新元素,该元素的值是 ix

vector对象(以及string对象)的下标运算符可用于访问已存在的元素,而不能用于添加元素。

通过下标访问不存在的元素会产生很严重的后果,所谓的缓冲区溢出(buffer overflow)指的就是这类错误。

确保下标合法的一种有效手段就是尽可能使用范围for语句。

3.4 迭代器介绍

之前学习了使用下标运算符访问 string 对象的字符或 vector 对象的元素;
使用**迭代器(iterator)**也可以实现同样的目的。
所有标准库容器都可以使用迭代器,但是其中只有少数几种才同时支持下标运算符。

3.4.1 使用迭代器

有迭代器的类型同时拥有返回迭代器的成员:

  • begin,负责返回指向第一个元素(或第一个字符)的迭代器;
  • end,负责返回指向容器(或 string 对象)“尾元素的下一位置”(one past the end)的迭代器;end成员返回的迭代器常被称作尾后迭代器(off-the-end iterator)或者简称为尾迭代器(end iterator)。
  • 如果容器为空,则beginend返回的是同一个迭代器,都是尾后迭代器。
/* 由编译器决定 b 和 e的类型,使用 auto 关键字定义变量 b 和 e */
auto b = v.begin(), e = v.end();    // b 和 e 的类型相同

迭代器运算符

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

如果两个迭代器指向的元素相同或者都是同一个容器的尾后迭代器,则它们相等;否则就说这两个迭代器不相等。

通过解引用迭代器来获取它所指示的元素:

/* 将 string 对象的第一个字母改为大写形式 */
string s("some string");
if (s.begin() != s.end()) {
      // 首先检查 s 是否为空,确保 s 非空
    auto it = s.begin();    // it 表示 s 的第一个字符
    *it = toupper(*it);     // 将当前字符改成大写形式
}
// Output: Some string

将迭代器从一个元素移动到另外一个元素

Note: 因为end返回迭代器并不实际指示某个元素,所以不能对其进行递增或解引用的操作。

/* 将 string 对象中第一个单词改写为大写形式*/
// 依次处理 s 的字符直至我们处理完全部字符或者遇到空白
for (auto it = s.begin(); it != s.end() && !isspace(*it); ++it)
    *it = toupper(*it);     // 将当前字符改成大写形式

循环部分首先用s.begin的返回值来初始化it,意味着it指示的是s中的第一个字符(如果有的话)。
条件部分检查是否已到达s的尾部,如果尚未到达,则将it解引用的结果传入isspace函数检查是否遇到了空白。

关键概念:泛型编程:C++程序员习惯性地使用!=而非<,是因为只有 string 和 vector 等一些标准库类型有下标运算符,大多数都没有定义<运算符,但是所有标准库容器的迭代器都定义了==!=。所以要养成使用迭代器和!=的习惯。

迭代器类型

就像不知道stringvectorsize_type成员到底是什么类型一样,一般来说我们也不知道或无须知道迭代器的精确类型。

实际上,那些拥有迭代器的标准库类型使用iteratorconst_iterator来表示迭代器的类型。

vector<int>::iterator it;   // it 能读写 vector 的元素
string::iterator it2;       // it2 能读写 string 对象中的字符

vector<int>::const_iterator it3;    // it3 只能读元素,不能写元素
string::const_iterator it4;         // it4 只能读元素,不能写元素
  • iterator 的对象可读可写
  • const_iterator 能读取但不能修改它所指的元素值

如果vector对象或string对象是一个常量,只能使用const_iterator
如果vector对象或string对象不是常量,那么既能使用iterator也能使用const_iterator

beginend运算符

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

为了专门得到const_iterator类型的返回值,C++新标准引入两个新函数,分别是cbegincend
如果对象不是常量,返回iterator;如果对象是常量,beginend返回const_iterator
不论 vector 对象(或 string 对象)本身是否是常量,返回值都是 const_iterator

结合解引用和成员访问操作

解引用迭代器可获得迭代器所指的对象,如果该对象的类型恰好是类,就有可能希望进一步访问它的成员。

对于一个由字符串组成的vector对象来说,要想检查其元素是否为空,令it是该vector对象的迭代器,只需检查it所指字符串是否为空就可以了:(*it).empty()

(*it).empty()   // 解引用 it,然后调用结果对象的 empty 成员
*it.empty()     // 错误:试图访问 it 的名为 empty 的成员,但 it 是个迭代器,没有 empty 成员

注意:(*it).empty() 圆括号必不可少。该表达式的含义是先对it解引用,然后解引用的结果再执行点运算符。如果不加圆括号,点运算符将由it来执行,而非it解引用的结果

上面第二个表达式的含义是从名为it的对象中寻找其empty成员,显然it是一个迭代器,它没有哪个成员叫empty的,所以第二个表达式将发生错误。

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

例:假设用一个名为 text 的字符串向量存放文本文件中的数据,其中的元素或者是一句话或者是一个用于表示段落分隔的空字符串。如果要输出 text 中第一段的内容,可以利用迭代器写一个循环令其遍历 text,直到遇到空字符串的元素为止:

// 依次输出 text 的每一行直至遇到第一个空白行为止
for (auto it = text.cbegin(); it != text.cend() && !it->empty(); ++it)
    cout << *it << endl;

值得注意的是,因为循环从头到尾只是读取 text 的元素而未向其中写值,所以使用了cbegincend来控制整个迭代过程。
当需要更改 vector 对象的内容,所以使用的迭代器应该是beginend,而非 cbegin 和 cend。

练习 3.22:修改之前那个输出 text 第一段的程序,首先把 text 的第一段全都改成大写形式,然后再输出它。

/* 练习 3.22:修改之前那个输出 text 第一段的程序,首先把 text 的第一段全都改成大写形式,然后再输出它。*/

/* 思路:需要将第一段(vector对象第一个空串元素之前的所有元素)改写成大写字母的形式再输出。
因为需要更改 vector 对象的内容,所以使用的迭代器应该是 begin 和 end,而非 cbegin 和 cend。*/

#include 
#include 
#include 

using namespace std;

int main()
{
     
    vector<string> text;
    string s;
    // 利用 getline 读取一句话,直接回车产生一个空串,表示段落结束
    while (getline(cin, s))
        text.push_back(s);
    // 利用迭代器遍历全部字符串,遇空串停止循环
    for (auto it = text.begin(); it != text.end() && !it -> empty(); it++)
    {
     
        // 利用迭代器遍历当前字符串
        for (auto it2 = it -> begin(); it2 != it -> end(); it2++)
            *it2 = toupper(*it2);   // 利用toupper改写成大写形式
        cout << *it << endl;        // 输出当前字符串
    }
    return 0;
}

练习3.23:编写一段程序,创建一个含有 10 个整数的 vector 对象,然后使用迭代器将所有元素的值都变成原来的两倍。

/* 练习3.23:编写一段程序,创建一个含有 10 个整数的 vector 对象,然后使用迭代器将所有元素的值都变成原来的两倍。
输出 vector 对象的内容,检验程序是否正确。*/

#include 
#include 
#include 
#include 

using namespace std;

int main()
{
     
    vector<int> vInt;
    srand((unsigned)time(NULL));    // 生成随机数种子
    for (int i = 0; i < 10; i++)
    {
     
        vInt.push_back(rand() % 1000);  // 每次循环生成一个1000以内的随机数并添加到vInt中
    }

    cout << "The 10 randomly generated numbers are: " << endl;
    // 利用常量迭代器读取原始数据
    for(auto it = vInt.cbegin(); it != vInt.cend(); it++)
    {
     
        cout << *it << " ";     // 输出当前数字
    }
    cout << endl;

    cout << "The 10 doubled numbers are: " << endl;
    // 利用非常量迭代器修改 vInt 内容并输出
    for (auto it = vInt.begin(); it != vInt.end(); it++)
    {
     
        *it *= 2;
        cout << *it << " ";     // 输出当前数字
    }
    cout << endl;

    return 0;
}

某些对vector对象的操作会使迭代器失效

  • 不能在范围for循环中向vector对象添加元素
  • 任何一种可能改变vector对象容量的操作,比如push_back,都会使该vector对象的迭代器失效

谨记:但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。

3.4.2 迭代器运算 (iterator arithmetic)

vector 和 string 迭代器支持的运算

迭代器的算术运算

  • iter + n
  • iter - n
  • iter1 += n
  • iter1 -= n
  • iter1 - iter2
  • >>=<<=

只要两个迭代器指向的是同一个容器中的元素或者尾元素的下一位置,就能将其相减,所得结果是两个迭代器的距离。所谓举例指的是右侧的迭代器向前移动多少位置就能追上左侧的迭代器,其类型是名为 difference_type 的带符号整型数。string 和 vector 都定义了 difference_type,因为这个举例可正可负,所以 difference_type 是带符号类型的。

使用迭代器运算

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

// text 必须是有序的
// beg 和 end 表示我们搜索的范围
auto beg = text.begin(), end = text.end();
auto mid = text.begin() + (end - beg)/2; // 初始状态下的中间点
// 当还有元素尚未检查并且我们还没有找到 sought 时执行循环
while (mid != end && *mid != sought) {
     
    if (sought < *mid)  // 我们要找的元素在前半部分吗?
        end = mid;      // 如果是,调整搜索范围使得忽略掉后半部分
    else                // 我们要找的元素在后半部分
        beg = mid + 1;  // 在 mid 之后找
    mid = beg + (end - beg)/2;  // 新的中间点
}

循环部分先检查搜索范围是否为空,如果 mid 和 end 的当前值相等,说明已经找遍了所有元素。此时条件不满足,循环终止。当搜索范围不为空,可知 mid 指向了某个元素,检查该元素是否就是我们所要搜索的,如果是,也终止循环。

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

练习3.26:在这个二分搜索程序中,为什么用的是mid = beg + (end - beg) / 2,而非mid = (beg + end) / 2

解答:
C++并没有定义两个迭代器的加法运算,实际上直接把两个迭代器加起来是没有意义的。

与之相反,C++定义了迭代器的检法运算,两个跌大气相减的结果是它们之间的距离,也就是说,将运算符右侧的迭代器向前移动多少个元素后可以得到左侧的迭代器,参与运算的两个跌大气必须指向同一个容器中的元素或尾后元素。

另外,C++还定义了迭代器与整数的加减法运算,用以控制迭代器在容器中左右移动。

在本题中,因为迭代器的加法不存在,所以mid = (beg + end) / 2;不合法。
mid = beg + (end - beg) / 2;的含义是,先计算end - beg的值得到容器中的元素个数,然后控制迭代器从开始处向右移动二分之一容器的长度,从而定位到容器正中间的元素。

练习 3.38:将两个指针相加不但是非法的,而且也什么意义。请问为什么两个指针相加没什么意义?110:

【出题思路】与标准库vector类似,C++也为指针定义了一系列算术运算,包括递增、递减、指针求差、指针与整数求和等,但是并没有定义两个指针的求和运算。
要想理解这一规定,必须首先明白指针的含义。

【解答】
指针也是一个对象,与指针相关的属性有 3 个,分别是指针本身的值(value)、指针所指的对象(content)、以及指针本身在内存中的存储位置(address)。它们的含义分别是:

  • 指针本身的值是一个内存地址值,表示指针所指对象在内存中的存储地址;
  • 指针所指的对象可以通过解引用指针访问;
  • 因为指针也是一个对象,所以指针也存储在内存的某个位置,它有自己的地址,这也是为什么有“指针的指针”的原因。

通过上述分析我们知道,指针的值是它所指对象的内存地址,如果我们把两个指针加在一起,就是试图把内存中两个对象的存储地址加在一起,这显然是没有任何意义的。
与之相反,指针的减法是有意义的。如果两个指针指向同一个数组中的不同元素,则它们相间的结果表征了它们所指的元素在数组中的距离。

练习3.24:请使用迭代器重做3.3.3节(第94页)的最后一个练习(P101)。

/* 练习3.24:请使用迭代器重做 3.3.3 节(第94页)的最后一个练习。P101 */

/* 练习3.20:读入一组整数并把它们存入一个 vector 对象,将每对相邻整数的和输出出来。
改写你的程序,这次要求先输出第 1 个和最后 1 个元素的和,接着输出第 2 个和倒数第 2 个元素的和,以此类推。*/

// 求相邻元素和的程序如下所示:
#include 
#include 

using namespace std;

int main()
{
     
    vector<int> vInt;
    int iVal;
    cout << "Please enter a group of numbers: " << endl;
    while (cin >> iVal)
        vInt.push_back(iVal);

    if (vInt.cbegin() == vInt.cend())
    {
     
        cout << "There are no elements." << endl;
        return -1;
    }
    cout << "Adjacent two numbers are: " << endl;
    // 利用 auto 推断 it 的类型
    for (auto it = vInt.cbegin(); it != vInt.cend() - 1; it++)
    {
     
        // 求相邻两项的和
        cout << *it + *(++it) << " ";
        // 每行输出 5 个数字
        if ((it - vInt.cbegin() + 1) % 10 == 0)
            cout << endl;
    }
    // 如果元素数是奇数,单独处理最后一个元素
    if (vInt.size() % 2 != 0)
        cout << *(vInt.end() - 1);
    return 0;
}

/*
Please enter a group of numbers:
5
548
45
987
87
74
15
^Z
Adjacent two numbers are:
553 1032 161 15
*/

// 求首尾元素和的程序如下所示:
#include 
#include 

using namespace std;

int main()
{
     
    vector<int> vInt;
    int iVal;
    cout << "Please enter a group of numbers: " << endl;
    while (cin >> iVal)
        vInt.push_back(iVal);
    if (vInt.cbegin() == vInt.cend())
    {
     
        cout << "There are no elements." << endl;
        return -1;
    }
    auto beg = vInt.begin();
    auto end = vInt.end();
    cout << "First and last elements are: " << *beg << " " << *(end-1) << endl;
    // 利用 auto 推断 it 的类型
    for  (auto it = beg; it != beg + (end - beg)/2; it++)
    {
     
        cout << (*it + *(beg + (end - it) - 1)) << " "; // ???
        // 每行输出 5 个数字
        if ((it - beg + 1) % 5 == 0)                    // ???
            cout << endl;
    }
    // 如果元素数是奇数,单独处理中间那个元素
    if(vInt.size() % 2 != 0)
        cout << *(beg + (end - beg)/2);
    return 0;
}

/*
Please enter a group of numbers:
5
548
45
987
87
74
15
^Z
First and last elements are: 5 15
20 622 132 987
*/

练习 3.25:3.3.3节(第93页)划分分数段的程序是使用下标运算符实现的,请利用迭代器改写该程序并实现完全相同的功能。

/* 以 10 分为一个分数段统计成绩的数量:0~9, 10~19, ..., 90~99, 100
得到各分数段的人数分布(成绩从低到高)*/

// 用下标运算符
vector<unsigned> scores(11, 0); // 11 个分数段,全都初始化为 0
unsigned grade;
while (cin >> grade) {
               // 读取成绩
    if (grade <= 100)           // 检查读入的成绩是否合法(即是否小于等于100分)
        ++scores[grade/10];     // 将成绩对应分数段的计数值加 1
}

// 用迭代器改写,通过解引用迭代器来获取它所指示的元素
vector<unsigned> scores(11);
auto it = scores.begin();
int grade;
while (cin >> grade)
    if (grade <= 100)
        ++*(it + grade/10);     // 利用迭代器定位到对应的元素,加 1
// 迭代器改写的完整代码
#include 
#include 

using namespace std;

int main()
{
     
    vector<unsigned> scores(11);
    auto it = scores.begin();
    int grade;
    cout << "Please enter a group of scores (0~100): " << endl;
    while (cin >> grade)
        if (grade <= 100)
            ++*(it + grade/10); // 利用迭代器定位到对应的元素,加 1
    cout << "You have inputted in total " << scores.size() << " scores." << endl;
    cout << "The number of people in each band (from low to high) is: " << endl;
    // 利用迭代器遍历 scores 的元素并逐个输出
    for (it = scores.begin(); it != scores.end(); it++)
    {
     
        cout << *it << " ";
    }
    cout << endl;

    return 0;
}

3.5 数组

与 vector 相似的地方是,数组也是存放类型相同的对象的容器;
与 vector 不同的地方是,数组的大小确定不变,不能随意向数组中增加元素。
Tip: 所以,如果不清楚元素的确切个数,请使用 vector。

练习 3.29:相比于vector来说,数组有哪些缺点,请列举一些。

解答:数组与vector的相似之处是hi都能存放类型相同的对象,且这些对象本身没有名字,需要通过其所在位置访问。

数组与vector的最大不同是,数组的大小固定不变,不能随意向数组种增加额外的元素,虽然在某些情景下运行时性能较好,但是与vector相比损失了灵活性。

具体来说:

  • 数组的维度在定义时已经确定,如果我们想更改数组的长度,只能创建一个更大的新数组,然后把原数组的所有元素复制到新数组中去。
  • 我们也无法像vector那样使用size函数直接获取数组的维度。
    • 如果是字符数组,可以调用strlen函数得到字符串的长度;
    • 如果是其他数组,只能使用sizeof(array)/sizeof(array[0])的方式计算数组的维度。

3.5.1 定义和初始化内置数组

数组的声明如 a[d],其中a是数组的名字,d是数组的维度。维度说明了数组中元素的个数,因此必须大于0
数组种元素的个数也属于数组类型的一部分,编译的时候维度应该是已知的。也就是说,维度必须是一个常量表达式。

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 时正确;否则错误

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

练习3.27:假设 txt_size 是一个无参数的函数,它的返回值是 int。

/* 练习3.27:假设 txt_size 是一个无参数的函数,它的返回值是 int。
请回答下列哪个定义是非法的?为什么? */

unsigned buf_size = 1024;

(a) int ia[buf_size];
// 非法,不是常量表达式,buf_size 是一个普通的无符号数,不是常量,不能作为数组的维度。

(b) int ia[4 * 7 - 14];
// 合法,4 * 7 - 14 是常量表达式

(c) int ia[txt_size()];
// 非法,txt_size() 是一个普通的函数调用,没有被定义为 constexpr,不能作为数组的维度

(d) char st[11] = "fundamental";
// 非法,当使用字符串初始化字符数组时,默认在尾部添加一个空字符 '\0',算上这个符号该字符共有 12 个字符,但是字符数组 st 的维度只有 11,无法容纳题目中的字符串。

显式初始化数组元素

可以对数组的元素进行列表初始化,此时允许忽略数组的维度。
如果在声明时没有指明维度,编译器会根据初始值的数量计算并推测出来;
相反,如果指明了维度,那么初始值的总数量不应该超出指定的大小。

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};          // 错误:初始值过多

字符数组的特殊性

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

char a1[] = {
     'c', '+', '+'};        // 列表初始化,没有空字符,维度是3
char a2[] = {
     'c', '+', '+', '\0'};  // 列表初始化,含有显式的空字符,维度是4
char a3[] = "C++";                  // 自动添加字符串结束的空字符,维度是4
const char a4[6] = "Daniel";        // 错误:没有空间可存放空字符!维度必须至少是7

不允许拷贝和赋值

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

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

理解复杂的数组声明

    1. 存放指针的数组
    1. 数组的指针
    1. 数组的引用

(数组本身是对象,所以允许定义数组的指针和数组的引用)

int *ptrs[10];  // ptrs 是存放有 10 个整型指针的数组
int &refs[10];  // 错误:不存在存放引用的数组,因为引用不是对象

int (*Parray)[10] = &arr;   // Parray 指向一个存放有 10 个整数的数组
int (&arrRef)[10] = arr;    // arrRef 引用一个存放有 10 个整数的数组

类型修饰符默认情况下从右向左依次绑定。
对于ptrs来说,从右向左理解其含义:首先知道我们定义的是一个大小为10 的数组,它的名字是ptrs,然后知道数组种存放的是指向int的指针。

当遇到指针指向数组或引用指向数组,由内往外阅读。
对于Parray来说,因为数组多维度是紧跟着被声明的名字的,所以这里应由内向外阅读。
首先是圆括号括起来的部分,*Parray意味着Parray是个指针,
接下来观察右边,可知道Parray是个指向大小为10的数组指针,
最后观察左边,知道数组种的元素是int
所以含义为:Parray是一个指针,它指向一个int数组,数组种包含10个元素。
同理,(&arrRef)表示arrRef是一个引用,它引用的对象是一个大小为10的数组,数组种元素的类型是int

int *(&arry)[10] = ptrs;   // arry 是数组的引用,该数组含有 10 个指针

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

3.5.2 访问数组元素

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

// 数组 用下标运算符
// 以 10 分为一个分数段统计成绩的数量:0~9,10~19,...,90~99,100
unsigned scores[11] = {
     };    // 11 个分数段,全部初始化为 0
unsigned grade;
while (cin >> grade) {
     
    if (grade <= 100)
        ++scores[grade/10]; // 将当前分数段的计数值加 1
}
// vector 用下标运算符
vector<unsigned> scores(11, 0); // 11 个分数段,全都初始化为 0
unsigned grade;
while (cin >> grade) {
               // 读取成绩
    if (grade <= 100)           // 检查读入的成绩是否合法(即是否小于等于100分)
        ++scores[grade/10];     // 将成绩对应分数段的计数值加 1
}

区别:

  • scores 的声明有不同,这里 scores 是一个含有 11 个无符号元素的数组。
    • 该程序对 scores 执行了列表初始化,为所有元素赋初值为 0,这样在后续统计时会从 0 开始计算各个分数段的人数,是正确的做法。
    • 如果不初始化 scores,则该数组会含有未定义的数值,这是因为 scores 是定义在函数内部的整形数组,不会执行默认初始化。
unsigned scores[11] = {
     };    // 11 个分数段,全部初始化为 0
vector<unsigned> scores(11, 0); // 11 个分数段,全都初始化为 0
  • 所用的下标运算符是由 C++ 语言直接定义的,这个运算符能用在数组类型的运算对象上,而上面的vector程序中所用的下标运算符是库模板 vector 定义的,只能用于 vector 类型的运算对象。

与 vector 和 string 一样,当需要遍历数组的所有元素时,最好的办法也是使用范围 for 语句。

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

练习 3.32:将上一题刚刚创建的数组拷贝给另外一个数组。

/* 练习 3.32:将上一题刚刚创建的数组拷贝给另外一个数组。
利用vector重写程序,实现类似的功能。*/

// 实现数组拷贝的程序如下所示:
#include 

using namespace std;

int main()
{
     
    const int sz = 10;  // 常量 sz 作为数组的维度
    int a[sz], b[sz];
    // 通过for循环为数组元素赋值
    for (int i = 0; i < sz; i++)
        a[i] = i;
    for (int j = 0; j < sz; j++)
        b[j] = a[j];
    // 通过范围 for 循环输出数组的全部元素
    for (auto val : b)
        cout << val << " ";
    cout << endl;

    return 0;
}


// 实现 vector 拷贝的程序如下所示:
#include 
#include 

using namespace std;

int main()
{
     
    const int sz = 10;  // 常量 sz 作为 vector 的容量
    vector<int> vInt, vInt2;
    // 通过for循环为 vector对象的元素赋值
    for (int i = 0; i < sz; i++)
        vInt.push_back(i);
    for (int j = 0; j < sz; j++)
        vInt2.push_back(vInt[j]);
    // 通过范围 for 循环输出 vector对象的全部元素
    for (auto val : vInt2)
        cout << val << " ";
    cout << endl;

    return 0;
}

检查下标的值

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

3.5.3 指针和数组

通过数组名字或者数组中首元素的地址都能得到指向首元素的指针:

  • 通常情况下,使用取地址符来获取指向某个对象的指针,取地址符可以用于任何对象。对数组使用取地址符就能得到指向该元素的指针
  • 数组还有一个特性:编译器都会自动地将其替换为一个指向数组首元素的指针
string nums[] = {
     "one", "two", "three"};    // 数组的元素是 string 对象
string *p = &nums[0];                       // p 指向 nums 的第一个元素

string *p2 = nums;  // 等价于 p2 = &nums[0],自动替换为一个指向数组首元素的指针
  • 当使用数组作为一个auto变量的初始值时,推断得到的类型是指针而非数组:
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作为初始值时,编译器实际执行的初始化过程类似于下面的形式:

auto ia2(&ia[0]);   // 显然 ia2 的类型是 int*
  • 必须指出的是,当使用decltype关键字时,上述转换不会发生,decltype(ia)返回的类型是由 10 个整数构成的数组:
decltype(ia) ia3 = {
     0,1,2,3,4,5,6,7,8,9};
ia3 = p;    // 错误:不能用整型指针给数组赋值
ia3[4] = i; // 正确:把 i 的值赋给 ia3 的一个元素

指针也是迭代器

vector 和 string 的迭代器支持的运算,数组的指针全都支持。

/* 例:允许使用递增运算符将指向数组元素的指针向前移动到下一个位置上 */
int arr[] = {
     0,1,2,3,4,5,6,7,8,9};
int *p = arr;   // p 指向 arr 的第一个元素
++p;            // p 指向 arr[1]

遍历数组中的元素:

先获取到指向数组第一个元素的指针和指向数组尾元素的下一位置的指针。

int *e = &arr[10];  // 指向 arr 尾元素的下一位置的指针

arr有 10 个元素,尾元素所在位置的索引是9,接下来那个不存在的元素唯一的用处就是提供其地址用于初始化e
就像尾后迭代器一样,尾后指针也不指向具体的元素。因此不能对尾后指针执行解引用或递增的操作。

/* 用数组的指针遍历,输出 arr 的全部元素 */
for (int *b = arr; b != e; ++b)
    cout << *b << endl; // 输出 arr 的元素

标准库函数beginend

尽管能计算得到尾后指针,但这种用法极易出错。
为了让指针的使用更简单、更安全,C++ 新标准引入了两个名为beginend的函数。
begin函数返回指向ia首元素的指针,end函数返回指向ia尾元素下一位置的指针,这两个函数定义在iterator头文件中。
这两个函数与容器中的两个同名成员功能类似,不过数组毕竟不是类类型,因此这两个函数不是成员函数。
正确的使用形式是将数组作为它们的参数

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

使用beginend可以很容易地写出一个循环并处理数组中的元素。例如,假设arr是一个整形数组,下面的程序负责找到arr中的第一个负数:

// pbeg 指向 arr 的首元素,pend 指向 arr 尾元素的下一位置
int *pbeg = begin(arr), *pend = end(arr);
// 寻找第一个负值元素,如果已经检查完全部元素,则结束循环
while (pbeg != pend && *pbeg >= 0) // 通过比较 pbeg 和 pend 来确保可以安全地对 pbeg 解引用
    ++pbeg;

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

指针运算

新指针指向的元素与原来的指针相比前进了(后退了)该整数值个位置:

constexpr size_t sz = 5;
int arr[sz] = {
     1,2,3,4,5};
int *ip = arr;      // 等价于 int *ip = &arr[0]
int *ip2 = ip + 4;  // ip2 指向 arr 的尾元素 arr[4]

注意:当给arr加上sz时,编译器自动地将arr转换成指向数组arr中首元素的指针。
执行加法后,指针从首元素开始向前移动了sz(这里是 5)个位置,指向新位置的元素。
也就是说,它指向了数组arr尾元素的下一位置。

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

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

int *b = arr, *e = arr + sz;
while (b < e) {
     
    // 使用 *b
    ++b;
}

只要两个指针指向同一个数组的元素,或者指向该数组的尾元素的下一位置,就能利用关系运算符对其进行比较。
如果两个指针分别指向不相关的对象,则不能比较它们:

int i = 0, sz = 42;
int *p = &i, *e = &sz;
// 未定义的:p 和 e 无关,因此比较毫无意义!
while (p < e)

注:上述指针运算同样适用于空指针所指对象并非数组的指针
如果p是空指针,允许给p加上或减去一个值为0的整型常量表达式。两个空指针也允许彼此相减,结果当然是0

解引用和指针运算的交互

指针加上一个整数所得的结果还是一个指针。
假设结果指针指向了一个元素,则允许解引用该结果指针:

int ia[] = {
     0,2,4,6,8}; // 含有 5 个整数的数组
int last = *(ia + 4);   // 正确:把 last 初始化成 8,也就是 ia[4] 的值

下标和指针

在很多情况下使用数组的名字其实用的是一个指向数组首元素的指针。
当对数组下标运算符时,编译器会自动执行上述转换操作。

ia[0] 是一个使用了数组名字的表达式,对数组执行下标运算其实是对指向数组元素的指针执行下标运算。

int ia[] = {
     0,2,4,6,8}; // 含有 5 个整数的数组
int i = ia[2];  // ia 转换成指向数组首元素的指针
                // ia[2] 得到 (ia + 2)所指的元素
int *p = ia;    // p 指向 ia 的首元素
i = *(p + 2);   // 等价于 i = ia[2]

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

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

注意:数组与标准库类型 string 和 vector 是有所不同的:

标准库类型 string 和 vector 限定使用的下标必须是无符号类型,
而内置的下标运算无此要求。上面的最后一个例子很好地说明了这一点。内置的下标运算符可以处理负值。

WARNING: 内置的下标运算符所用的索引值不是无符号数,这一点与 vector 和 string 不一样。

练习 3.36:编写一段程序,比较两个数组是否相等。再写一段程序,比较两个 vector 对象是否相等。

/* 练习 3.36:编写一段程序,比较两个数组是否相等。
再写一段程序,比较两个 vector 对象是否相等。

类似于一个彩票游戏,先由程序随机选出 5 个 0~9 的数字,次过程类似于摇奖;
再由用户手动输入 5 个猜测的数字,类似于购买彩票;
分别把两组数字存入数组 a 和 b,然后逐一比对两个数组的元素;
一旦有数字不一致,则告知用户猜测错误,
只有当两个数组的所有元素都相等时,判定数组相等,即用户猜测正确。
*/

#include 
#include 
#include 

using namespace std;

int main()
{
     
    const int sz = 5;   // 常量 sz 作为数组的维度
    int a[sz], b[sz], i = 0;
    srand( (unsigned) time (NULL) );    // 生成随机数种子

    // 通过 for 循环为数组元素赋值
    for (int i = 0; i < sz; i++)
    {
     
        // 每次循环生成一个 10 以内的随机数并添加到 a 中
        a[i] = rand() % 10;
    }

    cout << "Please enter 5 digits (0~9), with repetition: " << endl;
    int uVal;
    // 通过 for 循环为数组元素赋值
    for (i = 0; i < sz; i++)
        if (cin >> uVal)
            b[i] = uVal;

    cout << "The system generated data are: " << endl;
    for (auto val : a)
        cout << val << " ";
    cout << endl;

    cout << "Your guessed data are: " << endl;
    for (auto val : b)
        cout << val << " ";
    cout << endl;

    // 比较两个数组
    int *p = begin(a), *q = begin(b);   // 令 p 和 q 分别指向数组 a 和 b 的首元素
    while(p != end(a) && q != end(b))
    {
     
        if (*p != *q)
        {
     
            cout << "Your guess is wrong. The two arrays are not equal." <<endl;
            return -1;
        }
        p++;    // p 向后移动一位
        q++;    // q 向后移动一位
    }
    cout << "Congratulations, you have guessed them all right!" << endl;

    return 0;
}


/* 对比两个 vector 对象是否相等的程序如下所示,其中使用迭代器遍历 vector 对象的元素。*/

#include 
#include 
#include 
#include 

using namespace std;

int main()
{
     
    const int sz = 5;
    int i = 0;
    vector<int> a, b;
    srand( (unsigned) time (NULL));

    // 通过 for 循环为 vector 元素赋值
    for (int i = 0; i < sz; i++)
        a.push_back(rand() % 10);

    cout << "Please enter 5 digits (0~9), with repetition: " << endl;
    int uVal;
    // 通过 for 循环为 vector 元素赋值
    for (i = 0; i < sz; i++)
        if (cin >> uVal)
            b.push_back(uVal);

    cout << "The system generated data are: " << endl;
    for (auto val : a)
        cout << val << " ";
    cout << endl;

    cout << "Your guessed data are: " << endl;
    for (auto val : b)
        cout << val << " ";
    cout << endl;

    // 令 it1, it2 分别指向 vector 对象 a 和 b 的首元素
    auto it1 = a.cbegin(), it2 = b.cbegin();
    while (it1 != a.cend() && it2 != b.cend())
    {
     
        if (*it1 != *it2)
        {
     
            cout << "You have guessed them wrong. The two vectors are not equal." << endl;
            return -1;
        }
        it1++;  // p 向后移动一位
        it2++;  // q 向后移动一位
    }
    cout << "Congratulations, you have guessed them all right!" << endl;

    return 0;
}

3.5.4 C风格字符串

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

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

C 标准库 String 函数

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

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

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

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

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

练习3.37:以列表初始化方式赋值的C风格字符串与以字符串字面值赋值的有所区别:

// 一、以列表初始化方式赋值的 C风格字符串
const char ca[] = {
     'h', 'e', 'l', 'l', 'o', '\0'};
const char *cp = ca;
while (*cp) {
     
    cout << *cp << endl;
    ++cp;
}

// 二、以字符串字面值赋值的形式
const char ca[] = "hello";
const char *cp = ca;
while (*cp) {
     
    cout << *cp <<end;
    ++cp;
}

比较字符串

比较两个 C风格字符串的方法和之前学习过的比较标准库 string 对象的方法大相径庭。

  • (1) 比较标准库 string 对象的时候,用的是普通的关系运算符和相等性运算符:
string s1 = "A string example";
string s2 = "A different string";
if (s1 < s2)    // false: s2 小于 s1
  • (2) 比较两个 C 风格字符串时,实际比较的将是指针而非字符串本身:
// 错误做法
const char ca1[] = "A string example";
const char ca2[] = "A different string";
if (ca1 < ca2)  // 未定义的:试图比较两个无关地址

/* 解释:当使用数组的时候其实真正用的是指向数组首元素的指针,
因此,上面的 if 条件实际上比较的是两个 const char*的值。
这两个指针指向的并非同一对象,所以将得到未定的结果。  */
  • (3) 要想比较两个 C风格字符串 需要调用 strcmp 函数,此时比较的久不再是指针了。
    如果两个字符串相等,strcmp返回0
    如果前面的字符串较大,返回正值;如果后面的字符串较大,返回负值。
if (strcmp(ca1, ca2) < 0)   // 和两个 string 对象的比较 s1 < s2 效果一样

练习 3.39:编写一段程序,比较两个 string 对象。再编写一段程序,比较两个 C风格字符串的内容。

/* 练习 3.39:编写一段程序,比较两个 string 对象。
再编写一段程序,比较两个 C风格字符串的内容。*/

/*【出题思路】
由于标准库 string 类定义了关系运算符,所以比较两个 string 对象可以直接使用 <、>、==等;
比较两个C风格字符串则必须使用 cstring 头文件中定义的 strcmp 函数。
*/

// 比较两个 string 对象的程序如下所示:
#include 
#include 

using namespace std;

int main()
{
     
    string str1, str2;
    cout << "Please enter two strings: " << endl;
    cin >> str1 >>str2;

    if (str1 > str2)
        cout << "The first string is greater than the second string." << endl;
    else if (str1 < str2)
        cout << "The first string is smaller than the second string." << endl;
    else
        cout << "The two strings are equal." << endl;
    return 0;
}


// 比较两个 C风格字符串的程序如下所示,其中的分支部分选用了 switch-case 语句,其效果与上一个程序的 if-else 语句非常类似。
#include 
#include 

using namespace std;

int main()
{
     
    char str1[80], str2[80];
    cout << "PLease enter two strings: " << endl;
    cin >> str1 >> str2;
    // 利用 cstring 头文件中定义的 strcmp 函数比较大小
    auto result = strcmp(str1, str2);
    switch(result)
    {
     
    case 1:
        cout << "The first string is greater than the second string." << endl;
        break;
    case -1:
        cout << "The first string is smaller than the second string." << endl;
        break;
    case 0:
        cout << "The two strings are equal." << endl;
        break;
    default:
        cout << "Undefined result" << endl;
        break;
    }
    return 0;
}

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

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

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

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

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

C风格字符串的操作函数定义在cstring头文件中。

  • strcpy函数负责把字符串的内容拷贝给另一个字符串
  • strcat函数则负责把字符串的内容拼接到另一个字符串之后
  • strlen函数用于计算字符串的长度

需要注意的是,利用指腹从字面值常量初始化C风格字符串时,默认在数组最后添加一个空字符,因此strlen的计算结果比字面值显示的字符数量多1

为了细致起见,计算两个字符串拼接后的长字符串长度时,应该在两个字符串各自长度求和后减去1,即减去1个多余空字符所占的额外空间。

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

练习 3.40:编写一段程序,定义两个字符数组并用字符串字面值初始化它们;接着再定义一个字符数组存放前两个数组连接后的结果。使用 strcpy 和 strcat 把前两个数组的内容拷贝到第三个数组中。

/* 练习 3.40:编写一段程序,定义两个字符数组并用字符串字面值初始化它们;
接着再定义一个字符数组存放前两个数组连接后的结果。
使用 strcpy 和 strcat 把前两个数组的内容拷贝到第三个数组中。*/

#include 
#include 

using namespace std;

int main()
{
     
    char str1[] = "Welcome to ";
    char str2[] = "C++ family!";
    // 利用 strlen 函数计算两个字符串的长度,并求得结果字符串的长度
    char result[strlen(str1) + strlen(str2) - 1];

    strcpy(result, str1);   // 把第一个字符串拷贝到结果字符串中
    strcat(result, str2);   // 把第二个字符串拼接到结果字符串中

    cout << "The first string is: " << str1 << endl;
    cout << "The second string is: " << str2 << endl;
    cout << "The concatenated string is: " << result << endl;
    return 0;

/* Output:
The first string is: Welcome to
The second string is: C++ family!
The concatenated string is: Welcome to C++ family!
*/
}

3.5.5 与旧代码的接口

混用 string 对象和 C风格字符串

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

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

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

string s("Hello World");    // s 的内容是 Hello World
char *str = s;              // 错误:不能用 string 对象初始化 char*
const char *str = s.c_str();// 正确

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

使用数组初始化 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中的位置,
其中第二个指针应指向待拷贝区域尾元素的下一位置。
此例中,使用标准库beginend来分别计算int_arr的首指针和尾后指针。

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

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

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

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

练习3.41:编写一段程序,用整型数组初始化一个 vector 对象。

/* 练习3.41:编写一段程序,用整型数组初始化一个 vector 对象。*/

#include 
#include 
#include 
#include 

using namespace std;

int main()
{
     
    const int sz = 10;  // 常量 sz 作为数组的维度
    int a[sz];
    srand( (unsigned) time (NULL)); // 生成随机数种子

    cout << "The array content is: " << endl;
    // 利用范围for循环遍历 数组 的每个元素
    for (auto &val : a)
    {
     
        val = rand() % 100; // 生成一个 100 以内的随机数
        cout << val << " ";
    }
    cout << endl;

    // 利用 begin 和 end 初始化 vector 对象 (用数组初始化vector)
    vector<int> vInt(begin(a), end(a));
    cout << "The vector content is: " << endl;
    // 利用范围 for 循环遍历 vector 的每个元素
    for (auto val : vInt)
    {
     
        cout << val << " ";
    }
    cout << endl;

    return 0;
}

练习 3.42:编写一段程序,将含有整数元素的 vector 对象拷贝给一个整型数组。

/* 练习 3.42:编写一段程序,将含有整数元素的 vector 对象拷贝给一个整型数组。*/

/*【出题思路】
C++ 允许使用数组直接初始化 vector 对象,但是不允许使用 vector 对象初始化数组。
如果想用 vector 对象初始化数组,则必须把 vector 对象的每个元素逐一赋值给数组。
*/

#include 
#include 
#include 
#include 

using namespace std;

int main()
{
     
    const int sz = 10;  // 常量 sz 作为 vector 对象的容量
    vector<int> vInt;
    srand( (unsigned) time (NULL) ); // 生成随机数种子

    cout << "The vector content is: " << endl;
    // 利用for循环遍历 vector 对象的每个元素
    for (int i = 0; i != sz; i++)
    {
     
        vInt.push_back(rand() % 100);   // 生成一个 100 以内的随机数
        cout << vInt[i] << " ";
    }
    cout << endl;

    auto it = vInt.cbegin(); // 迭代器 it
    int a[vInt.size()];

    cout << "The array content is: " << endl;
    // 利用范围for循环遍历数组的每个元素
    for (auto &val : a)
    {
     
        val = *it; // 返回迭代器it所指元素的引用
        cout << val << " ";
        it++;
    }
    cout << endl;
    return 0;
}

/* Output:
The vector content is:
30 75 11 69 82 51 46 40 23 81
The array content is:
30 75 11 69 82 51 46 40 23 81
*/

3.6 多维数组

多维数组是数组的数组,一个维度表示数组本身大小,另外一个维度表示其元素(也是数组)大小:
对于二维数组来说,常把第一个维度称作行,第二个维度称作列。

int ia[3][4];   // 大小为 3 的数组,每个元素是含有 4 个整数的数组

// 大小为 10 的数组,它的每个元素都是大小为 20 的数组,
// 这些数组的元素是含有 30 个整数的数组
int arr[10][20][30] = {
     0};  // 将所有元素初始化为 0

多维数组的初始化

多维数组的每一行分别用花括号括了起来:

int ia[3][4] = {
         // 三个元素,每个元素都是大小为 4 的数组
    {
     0, 1, 2, 3},   // 第 1 行的初始值
    {
     4, 5, 6, 7},   // 第 2 行的初始值
    {
     8, 9, 10, 11}  // 第 3 行的初始值
};

或者:

// 没有标识每行的花括号,与之前的初始化语句是等价的
int ia[3][4] = {
     0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};

如果仅仅想初始化每一行的第一个元素,如下:

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

如果再省略掉内层的花括号,结果就大不一样:

// 显式地初始化第 1 行,其他元素执行值初始化
int ix[3][4] = {
     0, 3, 6, 9};

多维数组的下表引用

// 用 arr 的首元素为 ia 最后一行的最后一个元素赋值
int ia[2][3];           // 大小为 2 的数组,每个元素是含有 3 个整数的数组
int arr[10][20][30];    // 大小为 10 的数组,它的每个元素都是大小为 20 的数组,这些数组的元素是含有 30 个整数的数组
ia[2][3] = arr[0][0][0];
/*等号右侧的运算对象包含 3 个维度。首先通过索引 0 得到最外层的数组,它是一个大小为 20 的多维数组;
接着获取这 20 个元素数组的第一个元素,得到一个大小为 30 的一维数组;
最后再取出其中的第一个元素。 */

int (&row)[4] = ia[1];  // 把row绑定到ia的第二个4元素数组上
// 把 row 定义成一个含有 4 个整数的数组的引用,然后将其绑定到 ia 的第 2 行

程序中经常会用到两层嵌套的for循环来处理多维数组的元素:

constexpr size_t rowCnt = 3, colCnt = 4;
int ia[rowCnt][colCnt]; // 12 个未初始化的元素
// 对于每一行
for (size_t i = 0; i != rowCnt; ++i) {
     
    // 对于行内的每一列
    for (size_t j = 0; j != colCnt; ++j) {
     
        ia[i][j] = i * colCnt + j;
    }
}

使用范围 for 语句处理多维数组

用范围for循环重写上面的代码:

size_t cnt = 0;
for (auto &row : ia)        // 对于外层数组的每一个元素
    for (auto &col : row) {
      // 对于内层数组的每一个元素
        col = cnt;          // 将下一个值赋给该元素
        ++cnt;              // 将 cnt 加 1
    }

因为要改变元素的值,所以得把控制变量rowcol声明成引用类型。
其实还有一个深层次的原因,就是如果不是引用类型,编译器初始化时会自动将这些数组形式的元素转换成指向该数组内首元素的指针。

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

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

for (auto row : ia)
    for (auto col : row)

程序无法通过贬义,因为row不是引用类型,所以编译器初始化row时会自动将这些数组形式的元素(和其他类型的数组一样)转换成指向该数组内首元素的指针。这样得到的row的类型就是int*,显然内层的循环就不合法了,编译器将试图在一个int*内遍历。

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

指针和多维数组

因为多维数组实际上是数组的数组,所以由多维数组名转换得来的指针实际上是指向第一个内层数组的指针:

int ia[3][4];       // 大小为 3 的数组,每个元素是含有 4 个整数的数组
int (*p)[4] = ia;   // p 指向含有 4 个整数的数组
p = &ia[2];         // p 指向 ia 的尾元素

首先明确(*p)意味着p是一个指针。
接着观察右边发现,指针p所指的是一个维度为4的数组;
再观察左边,数组中的元素是整数。
因此,p是指向含有4个整数的数组的指针。

Note: 区别:

int *ip[4];     // 整型指针的数组
int (*ip)[4];   // 指向含有 4 个整数的数组

通过使用auto或者declytpe就能尽可能地避免在数组前面加上一个指针类型:

// 输出 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;

上面代码的分析:

外层的for循环首先声明一个指针p并令其指向ia的第一个内层数组,然后依次迭代直到ia的全部3行都处理完为止。
其中递增运算++p负责将指针p移动到ia的下一行。

内层的for循环负责输出内层数组所包含的值。它首先令指针q指向p当前所在行的第一个元素。
*p是一个含有4个整数的数组,数组名被自动地转换成指向该数组收元素的指针。
内层for循环不断迭代直到我们处理完了当前内层数组的所有元素为止。为了获取内层for循环的终止条件,再一次解引用p得到指向内层数组首元素的指针,给它加上4就得到了终止条件。

使用标准库函数beginend也能实现同样的功能,而且看起来更简洁一些:

// 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;
}

代码分析:

这一版本的程序中,循环终止条件由end函数负责判断。
虽然我们也能推断出p的类型是指向含有4个整数的数组的指针,q的类型是指向整数的指针,但是使用auto关键字我们就不必再烦心这些类型到底是什么了。

练习3.43:编写 3 个不同版本的程序,令其均能输出 ia 的元素。

/* 练习3.43:编写 3 个不同版本的程序,令其均能输出 ia 的元素。
版本1 使用范围for语句管理迭代过程;
版本2 和 版本3 都是用普通的 for 语句,
其中版本2 要求用下标运算符,版本3 要求用指针。
此外,在所有3个版本的程序中都要直接写出数据类型,而不能使用类型别名、auto 关键字或 decltype关键字。*/

#include 

using namespace std;

int main()
{
     
    int ia[3][4] = {
     0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
    cout << "利用范围for语句输出多维数组的内容: " << endl;
    for (int (&row)[4] : ia)
    {
     
        for (int &col : row)
            cout << col << " ";
        cout << endl;
    }

    cout << "利用普通for语句和下标运算符输出多维数组的内容: " << endl;
    for (int i = 0; i != 3; i++)
    {
     
        for (int j = 0; j != 4; j++)
            cout << ia[i][j] << " ";
        cout << endl;
    }

    cout << "利用普通for语句和指针输出多维数组的内容: " << endl;
    for (int (*p)[4] = ia; p != ia + 3; p++)
    {
     
        for (int *q = *p; q != *p + 4; q++)
            cout << *q << " ";
        cout << endl;
    }
    return 0;
}

类型别名简化多维数组的指针

using int_array = int[4];
typedef int int_array[4];

// 输出 ia 中每个元素的值,每个内层数组各占一行
for (int_array *p = ia; p != ia + 3; ++p) {
     
    for (int *q = *p; q != *p + 4; ++q)
        cout << *q << ' ';
    cout << endl;
}

// 程序将类型“4 个整数组成的数组” 命名为 int_array,用类型名 int_array 定义外层循环的控制变量让程序显得简洁明了。

练习3.44:改写上一个练习中的程序,使用类型别名来代替循环控制变量的类型。

/* 练习3.44:改写上一个练习中的程序,使用类型别名来代替循环控制变量的类型。 */

/* 练习3.43:编写 3 个不同版本的程序,令其均能输出 ia  的元素。
版本1 使用范围for语句管理迭代过程;
版本2 和 版本3 都是用普通的 for 语句,
其中版本2 要求用下标运算符,版本3 要求用指针。
此外,在所有3个版本的程序中都要直接写出数据类型,而不能使用类型别名、auto 关键字或 decltype关键字。*/

#include 

using namespace std;
using int_array = int[4];

int main()
{
     
    int ia[3][4] = {
     0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
    cout << "利用范围for语句输出多维数组的内容: " << endl;
    // for (int (&row)[4] : ia)
    for (int_array &row : ia)
    {
     
        for (int &col : row)
            cout << col << " ";
        cout << endl;
    }

    cout << "利用普通for语句和下标运算符输出多维数组的内容: " << endl;
    for (int i = 0; i != 3; i++)
    {
     
        for (int j = 0; j != 4; j++)
            cout << ia[i][j] << " ";
        cout << endl;
    }

    cout << "利用普通for语句和指针输出多维数组的内容: " << endl;
    // for (int (*p)[4] = ia; p != ia + 3; p++)
    for (int_array *p = ia; p != ia + 3; p++)
    {
     
        for (int *q = *p; q != *p + 4; q++)
            cout << *q << " ";
        cout << endl;
    }
    return 0;
}

练习3.45:再一次改写程序,这次使用 auto 关键字。

/* 练习3.45:再一次改写程序,这次使用 auto 关键字。*/

/* 练习3.44:改写上一个练习中的程序,使用类型别名来代替循环控制变量的类型。 */

/* 练习3.43:编写 3 个不同版本的程序,令其均能输出 ia  的元素。
版本1 使用范围for语句管理迭代过程;
版本2 和 版本3 都是用普通的 for 语句,
其中版本2 要求用下标运算符,版本3 要求用指针。
此外,在所有3个版本的程序中都要直接写出数据类型,而不能使用类型别名、auto 关键字或 decltype关键字。*/

#include 

using namespace std;

int main()
{
     
    int ia[3][4] = {
     0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
    cout << "利用范围for语句输出多维数组的内容: " << endl;
    // for (int (&row)[4] : ia)
    // for (int_array &row : ia)
    for (auto &row : ia)
    {
     
        for (auto &col : row)
            cout << col << " ";
        cout << endl;
    }

    cout << "利用普通for语句和下标运算符输出多维数组的内容: " << endl;
    for (auto i = 0; i != 3; i++)
    {
     
        for (auto j = 0; j != 4; j++)
            cout << ia[i][j] << " ";
        cout << endl;
    }

    cout << "利用普通for语句和指针输出多维数组的内容: " << endl;
    // for (int (*p)[4] = ia; p != ia + 3; p++)
    for (auto p = ia; p != ia + 3; p++)
    {
     
        for (auto q = *p; q != *p + 4; q++)
            cout << *q << " ";
        cout << endl;
    }
    return 0;
}

你可能感兴趣的:(C++,Primer,c++)