第3章 字符串、向量和数组
3.1 命名空间的using声明
形式
#include <iostream>
using std::cin; // std属于命名空间,cin是库函数
int main() {
int i;
cin >> i; // 有了using声明,所以不需要写成std::cin
std::cout << i << std::endl; // 这里的cout没有使用using声明,所以必须使用完整名称
return 0;
}
- 每个名字都需要独立的using声明
- 头文件不应包含using声明,因为如果头文件不经意间包含了一些名字,反而可能产生始料未及的名字冲突
3.2 标准库类型string
- 标准库类型string表示可变长的字符序列,使用string类型必须首先包含string头文件。作为标准库的一部分,string定义在命名空间std中
#include <string>
using std::string;
- c++标准库类型对于一般应用场合来说有足够的效率
3.2.1 定义和初始化string对象
直接初始化与拷贝初始化
- 如果使用等号(=)初始化一个变量,实际上执行的是拷贝初始化(copyinitialization),如果不使用等号,则执行的是直接初始化(direct initialization)。
string s1 = "hello"; // 拷贝初始化,s1是字面值"hello"的副本
string s2("hello"); // 直接初始化,s2是字面值"hello"的副本,除了字面值后面的空字符外
string s3 = s1; // 拷贝初始化,s3是s1的副本,等价于间接初始化:string s3(s1)
string s4(3, 'c'); // 直接初始化,s4为"ccc",等价于拷贝初始化:string s4 = string(3, 'c')
3.2.2 string对象上的操作
1.读写string对象
#include <iostream>
using namespace std;
int main() {
string s; // 定义一个名为s的空string
cin >> s; // 将标准输入的内容读取到s中。在执行读取操作时,string对象会自动忽略开头的空白(即 // 空格符、换行符、制表符等)并从第一个真正的字符开始读起,直到遇见下一处空白为止, // 例如输入" hello world ",则输出为"hello",输出结果中无任何空格
cout << s << end;
string s1, s2;
cin >> s1 >> s2; // 第一个字符串输入到s1,第二个输入到s2,以空格分割
cout << s1 << s2 << endl;
return 0;
}
2.读取未知数量的string对象
string word;
while(cin >> word) { // 反复读取,直至遇到文件结束标记或非法输入
cout << word << endl;
}
3.使用getline读取一行
- 目的:如果要在最终得到的字符串中保留输入时的空白符,这时应该用getline函数代替原来的>>运算符。
string line;
while (getline(cin, line)) {
cout << line << endl; // 函数从给定的输入流中读入内容,直到遇到换行符为止(注意换行符也被读进 // 来了),然后把所读的内容存入到那个string对象中去(注意不存换行符)
}
4.string的empty和size操作
- empty函数根据string对象是否为空返回一个对应的布尔值,使用点操作符指明是哪个对象执行了empty函数即可
// 每次读入一整行直至遇到空行为止
while (getline(cin, line)) {
if (!line.empty()) { // 逻辑非运算符(!)
cout << line << endl;
}
}
5.string::size_type类型
- 对于size函数来说,返回一个int或个unsigned似乎都是合情合理的。但其实size函数返回的是一个string::size_type类型的值。
- 它是一个无符号类型的值且能足够存放下任何string对象的大小。
- 所有用于存放string类的size函数返回值的变量,都应该是string::size_type类型的。
- 如果一条表达式中已经有了size()函数就不要再使用int了,这样可以避免混用int和unsigned可能带来的问题。
6.比较string对象
- 比较运算符逐一比较string对象中的字符,并且对大小写敏感
- 如果两个string对象的长度不同,而且较短string对象的每个字符都与较长string对象对应位置上的字符相同,就说较短string对象小于较长string对象。
- 如果两个string对象在某些对应的位置上不一致,则string对象比较的结果其实是string对象中第一对相异字符比较的结果。
7.为string对象赋值
- 在设计标准库类型时都力求在易用性上向内置类型看齐,因此大多数库类型都支持赋值操作
string str1(10, 'c'), str2;
str2 = str1; // 正确,str2是一个空字符串
8.两个string对象相加
string s1 = "hello", s2 = "world";
string s3 = s1 + s2; // s3的内容是helloworld
s1 += s2; // s1 = s1 + s2
9.字面值和string对象相加
string s1 = "hello", s2 = "world";
string s3 = s1 + "," + s2 + '\n'; // 正确
string s4 = s1 + ","; // 正确
string s5 = "hello" + ","; // 错误,每个加法运算符(+)的两侧的运算对象至少有一个是string
string s6 = s1 + "," + "world"; // 正确
string s7 = "hello" + "," + s2; // 错误,第一个加号两侧都为字面值,不能直接相加
- 因为某些历史原因,也为了与C兼容,所以C++语言中的字符串字面值并不是标准库类型string的对象。切记,字符串字面值与string是不同的类型。
3.2.3 处理string对象中的字符
1.cctype头文件
2.建议:使用C++版本的C标准库头文件
- C语言的头文件形如name.h,C++则将这些文件命名为cname
- cctype头文件和ctype.h头文件的内容是一样的,只不过从命名规范上来讲更符合C++语言的要求。特别的,在名为cname的头文件中定义的名字从属于命名空间std,而定义在名为.h的头文件中的则不然
- 一般来说,C++程序应该使用名为cname的头文件而不使用name.h的形式,标准库中的名字总能在命名空间std中找到
3.处理每个字符?使用基于范围的for语句
string s("hello world"); // 直接初始化
for (auto &c : s) { // 使用冒号来遍历
c = toupper(c); // c是引用,所以改变了c的值,把c中的字符都变为了大写
}
cout << s << endl;
4.只处理一部分字符?
- 使用下标:用
[]
,string对象的下标从0开始,下标的值称作“下标”或“索引”。如果string对象s至少包含两个字符,则s[0]是第1个字符、s[1]是第2个字符、s[s.size() -1]
表示最后一个字符。 - 使用迭代器
5.使用下标执行迭代
// 对于逻辑与运算符(&&),C++语言规定只有当左侧运算对象为真时才会检查右侧运算对象的情况。
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()的值就可以了。
3.3 标准库类型vector
要想使用vector(容器),必须包含适当的头文件
#include <vector>
using std::vector;
- C++语言既有类模板,也有函数模板,其中vector是一个类模板,而非类型,由vector生成的类型必须包含vector中元素的类型
- 编译器根据模板创建类或函数的过程称为实例化,通常会在模板名字后面跟一对尖括号,在括号内放上信息
- vector能容纳绝大多数类型的对象作为其元素,但是因为引用不是对象故不存在包含引用的vector
vector<int> ivect; // ivect保存int类型的对象
vector<Sales_item> Sales_vec; // 保存Sales_item类型对象
vector<vector<string>> file; // 该向量的元素是vector,过去必须在最后一个>前加空格
3.3.1 定义和初始化vector对象
vector<int> ivec; // 定义一个空vector
vector<int> ivec2(ivec); // 拷贝初始化,把ivec的元素拷贝给ivec2
vector<int> ivec3 = ivec2; // 拷贝初始化:把ivec的元素拷贝给ivec3
vector<string> svec(ivec); // 报错,类型不同
1.初始化vector对象
- C++11新标准提供了一种列表初始化方法为vector对象的元素赋初值
- 创建指定数量的元素初始化
- 直接初始化的方式适用于三种情况:初始值已知且数量较少、初始值是另一个vector对象的副本、所有元素的初始值都一样
vector<string> articles = {"a", "b", "c"}; // 用大括号括起来进行列表初始化,不能用小括号
vector<int> ivec1(10, -1); // ivec1包含了10个重复元素,值都为1
vector<int> ivec2{10, 1}; // ivec2有两个元素,分别为10和1
vector<int> ivec3(10); // 10个元素,ivec3每个初始化为0
vector<int> ivec3{10}; // 1个元素,该元素的值为10
vector<int> vi = 10; // 错误,必须使用直接初始化的形式指定向量大小
vector<string> svec1(10); // 10个元素,每个都是空string对象
vector<string> svec2{"hi"}; // 列表初始化,svec2有一个元素
vector<string> svec3("hi"); // 错误,不能使用字符串字面值构建vector对象
vector<string> svec4{10}; // svec4有10个初始化元素
vector<string> svec5{10, "hi"}; // svec5有10个hi值元素
3.3.2 向vector对象中添加元素
- 对于数量较多的vector来说,初始化方式就不合适了。更好的处理方法是先创建一个空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); // 依次把i值压到v2的尾端
}
string word;
vector<string> text;
while (cin >> word) {
text.push_back(word);
}
1.关键概念:vector对象能高效增长
- 当所有元素的值都一样时,定义vector时设定大小较好;但当元素的值有所不同,更有效的办法是先定义一个空的vector对象,再在运行时向其中添加具体值,如果先定义大小效果可能更差。
2.向vector对象添加元素蕴含的编程假定
- 如果循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环,具体原因将在5.4.3节解释。
- 范围for语句体内不应改变其所遍历序列的大小。
3.3.3 其他vector操作
- 可以通过访问元素在vector中的位置来处理vector对象中的元素
vector<int> v{1, 2, 3, 4, 5};
for (auto &i : v) { // i定义成引用类型,这样就能通过i给v的元素赋值,i的类型由auto关键字指定
i *= i; // *=把左侧运算对象和右侧运算对象相乘,结果存入左侧运算对象。
}
for (auto i : v) {
cout << i << " ";
}
cout << endl;
- vector的empty和size两个成员与string的同名成员功能一致:empty检查vector对象是否包含元素然后返回一个布尔值;size则返回vector对象中元素的个数,返回值的类型是由vector定义的size
vector<int>::size_type // 正确
vector::size_type // 错误
1.计算vector内对象的索引
- vector对象的下标也是从0开始计起,下标的类型是相应的size_type类型
2.不能用下标形式添加元素
- 不能通过下标去访问任何元素!如前所述,正确的方法是使用push_back
- vector对象(以及string对象)的下标运算符可用于访问已存在的元素,而不能用于添加元素。
- 通过下标访问不存在的元素的行为会导致缓冲区溢出(buffer overflow)错误
- 确保下标合法的一种有效手段就是尽可能使用范围for语句。
3.4 迭代器介绍
- 所有标准库容器都可以使用迭代器,但是其中只有少数几种才同时支持下标运算符。
- 类似于指针类型,迭代器也提供了对对象的间接访问。
3.4.1 使用迭代器
auto b = v.begin(), e = v.end(); // b表示v的第一个元素,e表示v为元素的尾元素的下一个位置
// end成员返回的迭代器常被称作尾后迭代器
- 如果容器为空,则begin和end返回的是同一个迭代器,都是尾后迭代器。
1.迭代器运算符
- 使用==和!=来比较两个合法的迭代器是否相等,如果两个迭代器指向的元素相同或者都是同一个容器的尾后迭代器,则它们相等;否则就说这两个迭代器不相等。
- 和指针类似,也能通过解引用迭代器来获取它所指示的元素
string s("some string");
if (s.begin() != s.end()) { // 确保s非空
auto it = s.begin();
*it = toupper(*it); // 将第一个字符更改为大写形式
}
2.将迭代器从一个元素移动到另外一个元素
- 迭代器使用递增(++)运算符来从一个元素移动到下一个元素。因为end返回的迭代器并不实际指示某个元素,所以不能对其进行递增或解引用的操作。
// 遍历s的字符直到遇到空白字符或s末尾为止
for (auto it = s.begin(); it != s.end() && !isspace(*it); ++it) {
*it = toupper(*it);
}
3.关键概念:泛型编程
- C++程序员习惯性地使用
!=
而非<
进行判断,其原因和他们更愿意使用迭代器而非下标的原因一样:因为这种编程风格在标准库提供的所有容器上都有效。
4.迭代器类型
- 那些拥有迭代器的标准库类型使用
iterator
和const_iterator
来表示迭代器的类型
vector<int>::iterator it; // it能读取vector<int>的元素
string::iterator it2; // it2能读写string中的字符
vector<int>::const_iterator it3; // it3只能读元素,不能写元素
string::const_iterator it4; // it4只能读字符,不能写字符
5.术语:迭代器和迭代器类型
- 迭代器这个名词有三种不同的含义:可能是迭代器概念本身,也可能是指容器定义的迭代器类型,还可能是指某个迭代器对象。每个容器类定义了一个名为iterator的类型,该类型支持迭代器概念所规定的一套操作。
6.begin和end运算符
- begin和end返回的具体类型由对象是否是常量决定,如果对象是常量,begin和end返回const_iterator;如果对象不是常量,返回iterator
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类型的返回值,C++11新标准引入了两个新函数,分别是cbegin和cend
auto it3 = v.cbegin(); // it3的类型是vector<int>::const_iterator
7.结合解引用和成员访问操作
(*it).empty()
该表达式的含义是先对it解引用,然后解引用的结果再执行点运算符,第一个小括号不能少
(*it).empty() //解引用it,然后调用对象的empty成员
*it.empty() // 错误:试图访问it名为empty的成员,但it是个迭代器,没有empty成员
- 为了简化上述表达式,C++语言定义了箭头运算符(->)
,it->mem
和(*it).mem
表达的意思相同。
8.某些对vector对象的操作会使迭代器失效
- 已知的一个限制是不能在范围for循环中向vector对象添加元素。另外一个限制是任何一种可能改变vector对象容量的操作,比如push_back,都会使该vector对象的迭代器失效。因此凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。
3.4.2 迭代器运算
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SBN7tTE3-1656400571134)(C++Primer学习笔记.assets/image-20220621161754373.png)]
1.迭代器的算术运算
- 可以令迭代器和一个整数值相加(或相减),其返回值是向前(或向后)移动了若干个位置的迭代器。
auto mid = v.begin() + v.size() / 2; // mid指向v的中间位置
- 只要两个迭代器指向的是同一个容器中的元素或者尾元素的下一位置,就能将其相减,所得结果是两个迭代器的距离。所谓距离指的是右侧的迭代器向前移动多少位置就能追上左侧的迭代器,其类型是名为difference_type的带符号整型数。string和vector都定义了difference_type,因为这个距离可正可负,所以difference_type是带符号类型的。
2.使用迭代器运算
- 二分搜索
3.5 数组
- 与vector不同的地方是,数组的大小确定不变,不能随意向数组中增加元素。因为数组的大小固定,因此对某些特殊的应用来说程序的运行时性能较好,但是相应地也损失了一些灵活性。所以如果不清楚元素的确切个数,请使用vector。
3.5.1 定义和初始化内置数组
- 数组的声明形如
a[d]
,其中a是数组的名字,d是数组的维度。维度说明了数组中元素的个数,因此必须大于0。数组中元素的个数也属于数组类型的一部分,编译的时候维度应该是已知的,即维度必须是一个常量表达式。 - 默认情况下,数组的元素被默认初始化。
- 定义数组的时候必须指定数组的类型,不允许用auto关键字由初始值的列表推断类型。
- 和vector一样,数组的元素应为对象,因此不存在引用的数组
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时正确,否则错误
1.显式初始化数组元素
- 如果在声明时没有指明维度,编译器会根据初始值的数量计算并推测出来;
- 如果指明了维度,那么初始值的总数量不应该超出指定的大小。
- 如果维度比提供的初始值数量大,则用提供的初始值初始化靠前的元素,剩下的元素被初始化成默认值
const unsigned sz = 3;
int ial[sz] = {0, 1, 2}; // 含有3个元素
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}; // 错误,超出指定大小
2.字符数组的特殊性
char a1[] = {'C', '+', '+'}; // 列表初始化,没有空字符,维度是3
char a2[] = {'C', '+', '+', '\0'}; // 列表初始化,含有显示空字符,维度是4
char a3[] = "C++"; // 自动添加表示字符串结束的空字符,维度是4
const char a4[6] = "Daniel"; // 错误,右边占7个维度
3.不允许拷贝和赋值
- 不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值
int a[] = {0, 1, 2};
int a2[] = a; // 错误,不能用一个数组初始化另一个数组
a2 = a; // 不能用一个数组直接赋值给另一个数组
4.理解复杂的数组声明
int *ptrs[10];
int &refs[10] = /* ? */; // 错误,不存在引用的数组
int (*Parray)[10] = &arr; // Parray指向一个含有10个整数的数组
int (&arrRef)[10] = arr; // arrRef引用一个含有10个整数的数组
int *(&arry)[10] = ptrs; // arry是数组的引用,按照由内向外的顺序阅读上述语句,首先知道arry是一个 // 引用,然后观察右边知道,arry引用的对象是一个大小为10的数组,最后观察 // 左边知道,数组的元素类型是指向int的指针。这样,arry就是一个含有10个 // int型指针的数组的引用
- 要想理解数组声明的含义,最好的办法是从数组的名字开始按照由内向外的顺序阅读。
3.5.2 访问数组元素
1.访问数组元素
- 数组除了大小固定这一特点外,其他用法与vector基本类似,数组的索引从0开始
// 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和string一样,当需要遍历数组的所有元素时,最好的办法也是使用范围for语句。
- 因为维度是数组类型的一部分,所以系统知道数组scores中有多少个元素,使用范围for语句可以减轻人为控制遍历过程的负担。
for (auto i : scores) {
cout << i << " "; // 输出当前的计数值
}
cout << endl;
2.检查下标的值
-
下标应该大于等于0而且小于数组的大小
-
要想防止数组下标越界,除了小心谨慎注意细节以及对代码进行彻底的测试之外,没有其他好办法
-
大多数常见的安全问题都源于缓冲区溢出错误。当数组或其他类似数据结构的下标越界并试图访问非法内存区域时,就会产生此类错误。
3.5.3 指针和数组
- 通常情况下,使用取地址符来获取指向某个对象的指针,取地址符可以用于任何对象。数组的元素也是对象,对数组的元素使用取地址符就能得到指向该元素的指针。
string nums[] = {"one", "two", "three"};
string *p = &nums[0]; // p指向nums的第一个元素
string *p2 = nums; // 在很多用到数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指 // 针:等价于p2 = &nums[0]
int ia[] = {0, 1, 2};
auto ia2(ia); // ia2是一个指向ia第一个元素的整型指针
decltype(ia) ia3 = {1, 2, 3}; // ia3是一个数组,不同于auto
ia3 = p; // 错误,不能用整形指针给数组赋值
1.指针也是迭代器
- vector和string的迭代器支持的运算,数组的指针全都支持
int arr[] = {0, 1, 2};
int *p = arr; // p指向arr的第一个元素
++p; // 允许使用递增运算符将指向数组元素的指针向前移动到下一个位置上,p指向arr[1]
int *e = &arr[3]; // 尾后指针,尾后指针不能执行解引用和递增操作。
for (int *b = arr; b != e; ++b) {
cout << *b << endl; // 输出arr的元素
}
2.标准库函数begin和end
- 为了让指针的使用更简单、更安全,C++11新标准引入了两个名为begin和end的函数。这两个函数与容器中的两个同名成员功能类似,不过数组毕竟不是类类型,因此这两个函数不是成员函数。正确的使用形式是将数组作为它们的参数。
int ia[] = {0, 1, 2};
int *beg = begin(ia); // 指向ia首元素
int *last = end(ia); // 指向ia的尾后元素
3.指针运算
- 给一个指针加上(减去)某整数值,结果仍是指针。新指针指向的元素与原来的指针相比前进了(后退了)该整数值个位置
- 和迭代器一样,两个指针相减的结果是它们之间的距离。参与运算的两个指针必须指向同一个数组当中的元素,两个指针相减的结果的类型是一种名为
ptrdiff_t
的标准库类型,和size_t
一样,ptrdiff_t
也是一种定义在cstddef
头文件中的机器相关的类型。因为差值可能为负值,所以ptrdiff_t
是一种带符号类型。
4.解引用和指针运算的交互
- 指针加上一个整数所得的结果还是一个指针。假设结果指针指向了一个元素,则允许解引用该结果指针:
int ia[] = {1, 2, 3};
int last = *(ia + 1); // 正确,把last初始化为2
last = *ia + 4; // 正确,last为5
5.下标和指针
ia[0]
是一个使用了数组名字的表达式,对数组执行下标运算其实是对指向数组元素的指针执行下标运算
int ia[] = {0, 2, 4};
int i = ia[2]; // ia转换成指向数组首元素的指着,ia[2]得到(ia + 2)所指的元素
int *p = &ia[1]; // p指向索引为1的元素
int j = p[1]; // j为ia[2]的元素值
int k = p[-1]; // k为ia[0]的元素值
- 虽然标准库类型string和vector也能执行下标运算,但是数组与它们相比还是有所不同。标准库类型限定使用的下标必须是无符号类型,而内置的下标运算无此要求,上面的最后一个例子很好地说明了这一点。内置的下标运算符可以处理负值。
3.5.4 C风格字符串
- 尽管C++支持C风格字符串,但在C++程序中最好还是不要使用它们。这是因为C风格字符串不仅使用起来不太方便,而且极易引发程序漏洞,是诸多安全问题的根本原因。
1.C标准库String函数
- 传入此类函数的指针必须指向以空字符作为结束的数组
char ca[] = {'C', '+', '+'}; // C风格字符串
cout << strlen(ca) << endl; // 严重错误,ca没有以空字符结束
2.比较字符串
- 要想比较两个C风格字符串需要调用strcmp函数,此时比较的就不再是指针了
const char ca1[] = "A string";
const char ca2[] = "A String";
if (ca1 < ca2) // 错误,当使用数组的时候其实真正用的是指向数组首元素的指针。因此上面的if条件实际
// 上比较的是两个const char*值。这两个指针指向的并非同一对象,所以将得到未定义的.
if (strcmp(ca1, ca2) < 0) // 如果两个字符串相等,strcmp返回0;如果前面的字符串较大,返回正值;如 // 果后面的字符串较大,返回负值
3.目标字符串的大小由调用者指定
- 连接或拷贝C风格字符串,正确的方法是使用
strcat
函数和strcpy
函数。不过要想使用这两个函数,还必须提供一个用于存放结果字符串的数组,该数组必须足够大以便容纳下结果字符串及末尾的空字符。 - 对大多数应用来说,使用标准库string要比使用C风格字符串更安全、更高效。
3.5.5 与旧代码的接口
- 现代的C++程序不得不与那些充满了数组和/或C风格字符串的代码衔接,为了使这一工作简单易行,C++专门提供了一组功能
1.混用string对象和C风格字符串
string s("Hello World");
char *str = s; // 错误,不能使用string对象初始化char*
const char *str = s.c_str(); // 正确
c_str
函数的返回值是一个C风格的字符串。也就是说,函数的返回结果是一个指针,该指针指向一个以空字符结束的字符数组,而这个数组所存的数据恰好与那个string对象的一样。结果指针的类型是const char*
,从而确保我们不会改变字符数组的内容。
2.使用数组初始化vector对象
- 允许使用数组来初始化vector对象。要实现这一目的,只需指明要拷贝区域的首元素地址和尾后地址就可以了
int int_arr[] = {0, 1, 2};
vector<int> ivec(begin(int_arr), end(int_arr));
3.建议:尽量使用标准库类型而非数组
- 现代的C++程序应当尽量使用vector和迭代器,避免使用内置数组和指针;应该尽量使用string,避免使用C风格的基于数组的字符串。
3.6 多维数组
- 严格来说,C++语言中没有多维数组,通常所说的多维数组其实是数组的数组。
int ia[3][4]; // 由内而外的顺序阅读,定义一个大小为3的数组,该数组的每个元素都是含有4个整数的数组。
1.多维数组的初始化
int ia[3][4] = {
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11}
};
// 上述内层嵌套着的花括号并非必需的,例如下面的初始化语句,形式上更为简洁,等价于上个数组
int ia2[3][4] = {0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11}
int ia3[3][4] = {{ 0 }, { 4 }, { 8 }}; // 初始化每一行的第一个元素,其他元素执行默认值初始化
int ia4[3][4] = {0, 3, 6, 9}; // 初始化的是第一行的4个元素,其他元素被初始化为0
2.多维数组的下标引用
- 如果表达式含有的下标运算符数量和数组的维度一样多,该表达式的结果将是给定类型的元素;如果表达式含有的下标运算符数量比数组的维度小,则表达式的结果将是给定索引处的内层数组
int (&row)[4] = ia[1]; // 把row绑定到ia的第二个4元素的数组上
constexpr size_t rowCnt = 3, colCnt = 4;
int ia[rowCnt][colCnt];
// 遍历赋值
for (size_t i = 0; i != rowCnt; ++i) {
for (size_t j = 0; j != colCnt; ++j) {
ia[i][j] = i *colCnt +j;
}
}
- size_t是一些C/C++标准在
stddef.h
中定义的,size_t类型表示C中任何对象所能达到的最大长度,它是无符号整数。
3.使用范围 for语句处理多维数组
- 由于在C++11新标准中新增了范围for语句
constexpr size_t rowCnt = 3, colCnt = 4;
int ia[rowCnt][colCnt];
size_t cnt = 0;
for (auto &row : ia) {
for (auto &col : row) {
col = cnt;
++cnt;
}
}
- 第一个for循环遍历
ia
的所有元素,这些元素是大小为4的数组,因此row的类型就应该是含有4个整数的数组的引用。第二个for循环遍历那些4元素数组中的某一个,因此col的类型是整数的引用。每次迭代把cnt
的值赋给ia
的当前元素,然后将cnt
加1。 - 要使用范围for语句处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型
4.指针和多维数组
int ia[3][4];
int (*p)[4] = ia; // p指向含有4个整数的数组
p = &ia[2]; // p指向a的为元素
- 现代的C++程序应当尽量使用vector和迭代器,避免使用内置数组和指针;应该尽量使用string,避免使用C风格的基于数组的字符串。