第3章 字符串、向量和数组
介绍两种最重要的标准库类型:string和vector,以及内置数组类型。
3.1命名空间的using声明
std::cin
表示使用命名空间std中的名字cin,也可用using namespace::name;
来声明命名空间,有了using声明就无须专门的前缀namespace::
也能使用所需名字。
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
// 每个名字都需要独立的using声明
int main()
{
cout << "Enter two numbers:" << endl;
int v1, v2;
cin >> v1 >> v2;
cout << "The sum of " << v1 << " and " << v2
<< " is " << v1 + v2 << endl;
return 0;
}
位于头文件的代码一般不使用using声明,因为头文件的内容会拷贝到所有引用它的文件中,如果头文件中有某个using声明,那每个使用头文件的文件都会有,这样会不经意间造成名字冲突。
3.2 标准库类型string
string表示可变长的字符序列,使用这种类型需首先包含string头文件#include <string>
。string作为标准库的一部分,定义在命名空间std中using std::string;
。
3.2.1 定义和初始化string对象
string s1; // 默认初始化,s1是空串
string s2 = s1; // 等价于"string s2(s1);",s2是s1的副本
string s3 = "value"; // 等价于"string s3("value");",s3是字面值"value"的副本,除了字面值最后的空字符
string s4(n, 'c'); // 把s4初始化为由连续n个字符c组成的串
直接初始化和拷贝初始化:使用“=”初始化变量执行的是拷贝初始化,编译器把“=”右侧的初始值拷贝到新创建的对象中去,不使用“=”的是直接初始化。
初始值只有一个时使用直接初始化或拷贝初始化都行,如果像s4初始化要用到的值有多个,则尽量使用直接初始化。
string s8(10, 'c');
// 上下初始化s8的语句都合法,但上方语句可读性更好
string temp(10, 'c');
string s8 = temp;
3.2.2 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 s1!=s2 | 判断s1是否和s2字符完全一样(对字母大小写敏感) |
<, <=, >, >= | 利用字符在字典中的顺序进行比较(对字母大小写敏感) |
读写string对象
可使用I/O操作符读写string对象,cin >> s
执行读取操作时,自动忽略开头的空白(即空格符、换行符、制表符等),从第一个真正的字符开始读起,直到遇见下一处空白为止。 多个输入输出可以连写在一起,输入时以空白分隔字符串。类似1.4.3节中的程序,可以用while语句读入未知数量的string对象。
使用getline读取一整行
使用getline
函数代替原来的>>
运算符可以保留输入时的空白符。函数的参数是一个输入流和一个string对象,函数从给定的输入流中读入内容,直到遇到换行符为止(换行符也被读进来了),然后把所读内容存入string对象(不存换行符)。getline(输入流, string对象)
一遇到换行符就结束读取操作并返回结果。
int main(){
string line;
// 每次读入一行,直到到达文件末尾,输出其中超过80个字符的行
while (getline(cin, line))
if (line.size() > 80)
cout << line << endl;
return 0;
}
string::size_type类型
大多数标准库都定义了几种配套的类型,这些配套类型体现了标准库类型与机器无关的特性,类型size_type是在类string中定义的,是一个无符号类型的值,因此如果一条表达式中已经有了size()函数就不要再使用int或其他类型的带符号整数了,这样可以避免混用带符号数和无符号数引起的错误。
比较string对象
按照字典顺序:
- 长度不同,较短string对象与较长string对象对应位置相等, 则较短string对象<较长string对象。
- 两个string对象在某些对应的位置上不一致,则比较的是string对象中第一对相异字符比较的结果。
字面值和string对象相加
当把string对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符+
的两侧的运算对象至少有一个是string:
string s1 = "hello", s2 = "world";
string s3 = s1 + ", " + s2 + '\n'; // s3为hello, world\n
string s4 = s1 + ", "; // 正确:string对象和一个字面值相加
string s5 = "hello" + ", "; // 错误:两个运算对象都不是string对象
string s6 = s1 + ", " + "world"; // 正确:每个加法运算符都有一个运算对象是string
// 等价于:string s6 = (s1 + ",") + "world";
string s7 = "hello" + ", " + s2; // 错误:不能把字面值直接相加
字符串字面值与string是不同的类型。
3.2.3 处理string对象中的字符
cctype头文件中的函数 | |
---|---|
isalnum(c) | 当c是字母或数字时返回true |
isalpha(c) | 当c是字母时返回true |
iscntrl(c) | 当c是控制字符时返回true |
isdigit(c) | 当c是数字时返回true |
isgraph(c) | 当c不是空格但可以打印时返回true |
islower(c) | 当c是小写字母时返回true |
isprint(c) | 当c是可打印字符时返回true(即c是空格或c具有可视形式) |
ispunct(c) | 当c是标点符号时返回true(不是控制字符、数字、字母、可打印空白中的一种) |
isspace(c) | 当c是空白时返回true |
isupper(c) | 当c是大写字母时返回true |
isxdigit(c) | 当c是十六进制数字时返回true |
tolower(c) | 如果c是大写字母,输出对应的小写字母,否则原样输出c |
toupper(c) | 如果c是小写字母,输出对应的大写字母,否则原样输出c |
C++兼容了C语言的标准库,C语言的头文件形如name.h,C++则将这些文件命名为cname,两个头文件内容是一样的,这里的c表示这是一个属于C语言标准库的头文件。标准库中的名字总能在命名空间std中找到。
使用基于范围的for语句处理每个字符
for (declaration : expression)
statement
其中expression
表示整个序列,每次迭代,declaration
变量会被初始化为expression
部分的下一个元素值。该循环可被读作“对于expression中的每一个基础元素declaration,执行statement操作”。
string str("some string");
// 每行输出str中的一个字符
for (auto c : str)
cout << c << endl;
使用范围for语句改变字符串中的字符
若想改变string对象中字符的值,必须把循环变量定义成引用类型。
string s("Hello World!!!");
// 转换成大写形式
for (auto &c : s) // c是引用
c = toupper(c); // 改变c即改变s中每个字符的值
cout << s << endl;
只处理一部分字符
访问string对象中的单个字符有两种方式:1、使用下标;2、使用迭代器。
下标运算符[]
接收的输入参数是string::size_type
类型的值,参数表示要访问的字符的位置,:返回值是该位置上字符的引用。
string对象的下标必须大于等于0而小于s.size()
一种可行的方法是总是设置下标的类型是string::size_type,因为该类型是无符号数,可确保下标不会小于0。
string s("some string");
if (!s.empty()) // 确保s[0]的位置确实有字符
s[0] = toupper(s[0]); // 为s的第一个字符赋新值
3.3 标准库类型vector
vector表示对象的集合,其中所有对象的类型都相同,每个对象都有一个与之对应的索引,vector也常被称为容器。使用vector必须包含适当的头文件,作声明如下:
#include <vector>
using std::vector;
C++有函数模板和类模板,vector是一个类模板,编译器可以根据模板创建类或函数,这个过程称为实例化,使用模板时需要指出编译器应该把类或函数实例化成何种类型,提供哪些信息来实例化由模板决定,通过在模板名字后面跟一对尖括号,在括号内放上信息。
// 根据模板vector生成三种不同的类型:vector<int>、vector<Sales_item>和vector<vector<string>>。
vector<int> ivec; // ivec保存int类型的对象
vector<Sales_item> Sales_vec; // 保存Sales_item类型的对象
vector<vector<string>> file; // 该向量的元素是vector对象
3.3.1 定义和初始化vector对象
初始化vector对象 | |
---|---|
vector<T> v1 | v1是一个空vector,其潜在的元素是T类型的,执行默认初始化。定义一个空vector,然后运行时获取到元素值后再逐一添加。 |
vector<T> v2(v1) | v2包含有v1所有元素的副本,等价于v2 = v1。两个vector对象的类型必须相同。 |
vector<T> v3(n, val) | v3包含了n个重复元素,每个元素的值都是val。 |
vector<T> v4(n) | v4包含了n个重复地执行了值初始化的对象。 *有些类型必须明确地提供初始值,如果vector对象中元素类型不支持默认初始化,就必须提供初始值。 **如果只提供了元素数量而没有设定初始值,只能使用直接初始化,即v4 = n这种方式是错误的。 |
vector<T> v5{a,b,c…} | v5包含了初始值个数的元素,每个元素被赋予相应的初始值,等价于v5={a,b,c…}。 |
3.3.2 向vector对象中添加元素
vector<int> v2; // 空vector对象
for (int i = 0; i != 100; ++i)
v2.push_back(i); // 利用vector的成员函数push_back向其中添加元素
// 未知元素确切个数,需要实时读入数据后将其赋给vector对象
string word;
vector<string> text;
while (cin >> word) {
text.push_back(word);
若不提前设置vector的大小,则当元素的值有所不同时,性能可能更差。一个高效的办法就是先定义一个空的vector对象,再在运行时向其中添加具体值。
范围for语句体内不应改变其所遍历序列的大小。循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环。
3.3.3 其他vector操作
vector支持的操作 | |
---|---|
v.empty() | 如果v不含有任何元素,返回true,否则返回false |
v.size() | 返回v中元素个数,由vector定义的size_type类型 |
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相等当且仅当它们的元素数量相同且对应位置的元素值都相同 |
<, <=, >, >= | 以字典顺序进行比较 |
vector<int> v{1,2,3,4,5,6,7,8,9};
for (auto &i : v) // 把i定义成引用类型,这样就能通过i给v的元素赋值
i *= i;
for (auto i : v)
cout << i << " ";
cout << endl;
vector对象(以及string对象)的下标运算符可用于访问已存在的元素,而不能用于添加元素。
3.4 迭代器介绍
类似于指针类型,迭代器也提供了对对象的间接访问。其对象是容器中的元素或者string对象中的字符,使用迭代器可以访问某个元素,迭代器也能从一个元素移动到另外一个元素。有效的迭代器或者指向某个元素,或者指向容器中尾元素的下一个位置,其他所有情况都属于无效。
3.4.1 使用迭代器
有迭代器的类型同时拥有返回迭代器的成员。
auto b = v.begin(), e = v.end();
// begin、end是类型的成员
// begin成员返回指向第一个元素的迭代器,end成员负责返回指向容器(或string对象)尾元素的下一个位置
end返回的是一个本不存在的元素,无实际含义,仅是个标记。如果容器为空,则begin和end返回的是同一个迭代器,都是尾后迭代器。
标准容器迭代器的运算符 | |
---|---|
*iter | 返回迭代器iter所指元素的引用 |
iter->mem | 解引用iter并获取该元素的名为mem的成员,等价于(*iter).mem |
++iter | 使iter指示容器中的下一个元素 |
--iter | 使iter指示容器中的上一个元素 |
iter1 == iter2 iter1 != iter2 | 判断两个迭代器是否相等(不相等),如果两个迭代器指示的是同一个元素或者它们是同一个容器的尾后迭代器,则相等;反之则不等 |
不能对end返回的迭代器进行递增或解引用的操作。
// 把string对象的第一个字母改为了大写形式
string s("some string");
if (s.begin() != s.end()) { // 确保s非空
auto it = s.begin(); // it表示s的第一个字符
*it = toupper(*it);
}
for (auto it = s.begin(); it != s.end() && !isspace(*it); ++it)
*it = toupper(*it);
上述代码中不在for循环中使用<的原因:因为所有标准库容器的迭代器都定义了==和!=,但大多数都没有定义<运算符。
迭代器类型
一般来说不知道迭代器的精确类型,但拥有迭代器的标准库类型使用iterator和const_iterator来表示迭代器的类型,const_iterator能读取但不能修改所指的元素值,iterator的对象可读可写:
vector<int>::iterator it; // it能读写vector<int>的元素
string::iterator it2; // it2能读写string对象中的字符
vector<int>::const_iterator it3; // it3只能读元素,不能写元素
string::const_iterator it4; // it4只能读字符,不能写字符
如果vector对象或string对象是一个常量,只能使用const_iterator;如果vector对象或string对象不是常量,则两种都能使用。
begin和end运算符
如果对象是常量,begin和end返回const_iterator;如果对象不是常量,返回iterator,但可使用cbegin
和cend
来专门得到const_iterator类型的返回值:
vector<int> v;
auto it1 = v.begin(); // it1的类型是vector<int>::iterator
auto it3 = v.cbegin(); // it3的类型是vector<int>::const_iterator
结合解引用和成员访问操作
解引用操作可以获得迭代器所指的对象,如果该对象的类型恰好是类,就有可能希望进一步访问它的成员:
(*it).empty() // 解引用it,然后调用结果对象的empty成员,等价于it->empty()
*it.empty() // 错误:()是必须的,此处试图访问it的名为empty的成员,但it是个迭代器,没有empty成员
例:text字符串向量中存放文本文件的数据,其中的元素是一句话或者是一个表示段落分隔的空字符,如果要输出第一段的内容,可利用迭代器写一个循环令其遍历text,直到遇到空字符串的元素为止:
for (auto it = text.cbegin(); it != text.cend() && !it->empty(); ++it)
cout << *it << endl;
// 循环从头到尾只是读取text的元素而未写值,所以使用了cbegin和cend来控制迭代过程
某些对vector对象的操作会使迭代器失效
- 不能在范围for循环中向vector对象添加元素。
- 任何一种可能改变vector对象容量的操作,比如push_back,都会使该vector对象的迭代器失效。
3.4.2 迭代器运算
vector和string迭代器支持的运算 | |
---|---|
iter + n iter - n | 仍得到一个迭代器,新位置与原来相比向前或向后移动了若干个元素,结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一个位置 |
iter1 += n iter1 -= n | 复合赋值语句,将iter1加/减的结果赋给iter1 |
iter1 - iter2 | 两个迭代器的距离,将右侧的迭代器向前移动差值个元素后得到左侧的迭代器(参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一个位置) |
>, >=, <, <= | 如果某迭代器指向的容器位置在另一个迭代器之前,则前者小于后者(参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一个位置) |
3.5 数组
与vector类似,数组也是存放类型相同的对象的容器,但数组的大小确定不变,不能随意向数组中增加元素。如果不确定元素的确切个数则使用vector。
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时正确,否则错误
显式初始化数组元素
int ial[3] = {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"; // 错误:结尾有一个空字符没有空间存放
理解复杂的数组声明
int arr[10];
int *ptrs[10]; // 从右向左,首先是大小为10的数组,名字是ptrs,存放指向int的指针
int &refs[10] = /* ?*/; // 错误:不存在引用的数组,因为引用不是对象
int (*Parray)[10] = &arr; // 由内向外阅读,*Parray意味着Parray是个指针,指向一个含有10个整数的数组
int (&arrRef)[10] = arr; // 由内向外阅读,&arrRef说明arrRef是个引用,引用的对象是一个大小为10的数组,数组中元素类型是int
int *(&arry)[10] = ptrs; // 由内向外阅读,arry是个引用,引用的对象是一个大小为10的数组,最后从左边得知数组的元素类型是指向int的指针
理解数组声明的含义可从数组的名字开始并按照从内向外的顺序阅读。
3.5.2 访问数组元素
与标准库类型vector和string一样,数组的元素能使用范围for语句或下标运算符访问。当需要遍历数组所有元素时,最好使用范围for语句,可以减轻人为控制遍历过程的负担。
3.5.3 指针和数组
使用数组的时候编译器一般会把它转换成指针。
string nums[] = {"one", "two", "three"};
string *p = &nums[0]; // p指向nums的第一个元素
string *p2 = nums; // 等价于p2 = &nums[0]
auto p3(nums); // p3是一个指针,指向nums的第一个元素
auto p3(&nums[0]); // 上述初始化过程类似该语句
// 当使用decltype关键字时,上述转换不会发,decltype(nums)返回的类型是由3个字符字面值构成的数组
指针也是迭代器
vector和string的迭代器支持的运算,数组的指针全都支持。在iterator头文件中引入了两个名为begin和end的函数:
int ia[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int *beg = begin(ia); // 指向ia首元素的指针
int *last = end(ia); // 指向ia尾元素的下一个位置的指针
指针运算
指向数组元素的指针可以执行所有迭代器运算,包括解引用、递增、比较、与整数相加、两个指针相减等。两个指针指向同一个数组的元素,或者指向该数组的尾元素的下一个位置,就能利用关系运算符对其进行比较,两个指针相减的结果类型是一种名为ptrdiff_t的标准库类型,定义在cstddef头文件中的带符号类型。
解引用和指针运算的交互
指针加上一个整数所得的结果还是一个指针:
int ia[] = {0, 2, 4, 6, 8};
int last = *(ia + 4); // 把last初始化为8,即ia[4]的值
int last = *ia + 4; // last = 4等价于ia[0] + 4
下标和指针
很多情况下,使用数组名字其实是用的一个指向数组元素的指针:
int ia[] = {0, 2, 4, 6, 8};
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)
int k = p[-2]; // p[-2]等价于*(p - 2),是ia[0]表示的元素
// 标准库类型限定使用的下标必须是无符号类型,而内置的下标运算无此要求,内置的下标运算可以处理负值
3.5.4 C风格字符串
C风格字符串不是一种类型,而是为了表达和使用字符串而形成的一种约定俗成的写法,按此写法,字符串存放在字符数组中并以空字符结束。
定义在cstring头文件的C风格字符串函数* | |
---|---|
strlen(p) | 返回p的长度,空字符不计算在内 |
strcmp(p1, p2) | 若p1 == p2,返回0,p1 > p2,返回正值,p1 < p2,返回负值 |
strcat(p1, p2) | 将p2附加到p1之后,返回p1,p1空间需足够 |
strcpy(p1, p2) | 将p2拷贝给p1,返回p1,p1空间需足够 |
- 允许使用以空字符结束的字符数组来初始化string对象或为string对象赋值。
- string对象的加法运算中允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个运算对象都是),在string对象的复合赋值运算中允许使用以空字符结束的字符数组作为右侧的运算对象。
若程序需要一个C风格字符串,则无法使用string对象来直接代替它,需要使用名为c_str
的成员函数:
string s("Hello World");
char *str = s; // 错误:不能使用string对象初始化char*
const char *str = s.c_str(); // 正确:c_str函数返回C风格的字符串
// 若后续的操作改变了s的值,就有可能让之前返回的数组失去效用
使用数组初始化vector对象
不允许使用一个数组为另一个内置类型的数组赋初值,也不允许使用vector对象初始化数组。相反,可以使用数组来初始化vector对象:
int int_arr[] = {0, 1, 2, 3, 4, 5};
vector<int> ivec(begin(int_arr), end(int_arr)); // 指明首元素地址和尾后地址
// ivec中元素为int_arr中对应元素的副本,创建ivec的两个指针指明了用来初始化的值在数组int_arr中的位置
vector<int> subVec(int_arr + 1, int_arr + 4);
// 拷贝3个元素:int_arr[1]、int_arr[2]、int_arr[3]
尽量使用标准库类型而非数组:使用指针和数组很容易出错,一部分因为指针常用于底层操作,其他问题则源于语法错误,特别是声明指针时的语法错误。现代C++程序应尽量使用vector和迭代器,避免使用内置数组和指针,应尽量使用string,避免使用C风格的基于数组的字符串。
3.6 多维数组
通常所说的多维数组其实是数组的数组。
int ia[3][4]; // 大小为3的数组,每个元素是含有4个整数的数组
// 对于二维数组,通常把第一个维度称作行,第二个维度称作列
多维数组的初始化
int ia[3][4] = {
{0, 1, 2, 3}, // 第1行的初始值
{4, 5, 6, 7},
{8, 9, 10, 11}
};
int ia[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
// 上面两条语句等价
int ia[3][4] = {{ 0 }, { 4 }, { 8 }}; // 显式初始化每行首元素,其他元素默认值初始化
多维数组的下标引用
ia[2][3] = arr[0][0][0]; // 用arr的首元素为ia的最后一行的最后一个元素赋值
int (&row)[4] = ia[1]; // row是一个含有4个整数的数组的引用,把row绑定到ia的第2个4元素数组上
用两层嵌套for循环处理多维数组元素:
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;
使用范围for语句处理多维数组
size_t cnt = 0;
for (auto &row : ia) // 遍历ia的所有元素,大小为4的数组
for (auto &col : row) { // 遍历4元素数组中的某一个
col = cnt; // 将下一个值赋给该元素
++cnt;
上述例子中因为要改变数组元素的值,所以选用引用类型作为循环控制变量,另一个原因:
for (const auto &row : ia)
for (auto col : row)
cout << col << endl;
/*
* 该循环没有写操作,但为避免数组被自动转为指针,还是将外层循环的控制变量声明成了引用类型
* 若不使用引用,则编译器初始化row时会自动将这些数组形式的元素转换成指向该数组内首元素的指针
* 这样得到的row类型就是int*,编译器试图在int*内遍历,显然不合法
*/
要使用范围for语句处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型。
指针和多维数组
多维数组是数组的数组,所以多维数组名转换得来的指针实际上是指向第一个内层数组的指针:
int ia[3][4];
int (*p)[4] = ia; // p指向含有4个整数的数组
p = &ia[2]; // p指向ia的尾元素
int *ip[4]; // 整型指针的数组
int (*ip2)[4]; // 指针指向含有4个整数的数组
通过使用auto或decltype可以避免在数组前加上一个指针类型
// 输出ia中每个元素的值
for (auto p = ia; p != ia + 3; ++p) { // p指向含有4个整数的数组
for (auto q = *p; q != *p + 4; ++q) // q指向4个整数数组的首元素
cout << *q << ' ';
cout << endl;
}
// 实现与上述代码相同的功能
for (auto p = begin(ia); p != end(ia); ++p) {
for (auto q = begin(*p); q != end(*p); ++q)
cout << *q << ' ';
cout << endl;
}
类型别名简化多维数组的指针
using int_array = int[4];
typedef int int_array[4]; // 与上一条语句等价
for (int_array *p = ia; p != ia + 3; ++p) {
for (int *q = *p; q != *p + 4; ++q)
cout << *q << ' ';
cout << endl;
}