C++ primer学习笔记——第三章 字符串、向量和数组

一、命名空间的using说明

作用域操作符(::)

using声明语句:

using namespace::name;

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

每个名字都需要独立的using声明,而且每句话都得以分号结束。

头文件中不应该包含using声明,以免产生始料未及的名字冲突。

二、标准库类型string

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

#include <string>
using namespace std;

1、定义和初始化string对象

初始化string对象的方式
string  s1
string s2 (s1)
string s2=s1
string s3("value")       s3是字面值"value"的副本,除了字面值最后的那个空字符外
string s3="value"
string s4(n,'c')            把s4初始化为由连续n个字符c组成的串

直接初始化和拷贝初始化:

如果用等号(=)初始化一个变量,实际上执行的是拷贝初始化,编译器把等号右侧的初始值拷贝到新创建的对象中去。

与之相反,如果不适用等号,则执行的是直接初始化。

当初始值只有一个时,使用直接初始化或拷贝初始化都行。如果像上面的s4那样初始化要用到的值有多个,一般来说只能使用直接初始化的方式:

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

2.string对象上的操作

string的操作
os<<s
is>>s
getline(is,s)       从is中读取一行赋给is,返回is
s.empty()          s为空返回true,否则返回false
s.size()             返回s中字符的个数
s[n]
s1+s2               返回s1和s2连接后的结果
s1=s2               用s2的副本代替s1中原来的字符
s1==s2            如果s1和s2中所含的字符完全一样,则他们相等;string对象
s1!=s2             的相等性判断对字母的大小写敏感
<,<=,>,>=        利用字符在字典中的顺序进行比较,且对字母的大小写敏感

读写string对象

在执行读写操作时,string对象会自动忽略开头的空白(即空格符、换行符、制表符等)并从第一个真正的字符开始读起,直到遇到下一处空白为止。

程序一:
#include <iostream>
#include <string>
using namespace std;

int main()
{
   string s;
   cin>>s;
   cout<<s<<endl;
   return 0;
}
程序二:
#include <iostream>
#include <string>
using namespace std;

int main()
{
   string s1,s2;
   cin>>s1>>s2;
   cout<<s1<<s2<<endl;
   return 0;
}

如果两个程序输入的都是“      Hello World!     ”,则程序一输出的将是“Hello”,输出结果中没有任何空格;程序二输出的将是“HelloWorld!”。

使用getline读取一整行:

有时我们希望能够在最终得到的字符串中保留输入时的空白符,这时应该用getline函数代替原来的>>运算符。getline函数的参数是一个输入流和一个string对象,函数从给定的输入流中读入内容,直到遇到换行符为止(注意换行符也被读进来了),然后把所读的内容存入到那个string对象中去(注意不存换行符)。getline只要一遇到换行符就结束读取操作并返回结果,哪怕输入的一开始就是换行符也是如此。如果输入真的一开始就是换行符,那么所得的结果是个空string。

int main()
{
   string line;
   while(getline(cin,line))
       cout<<line<<endl;
   return 0;
}

因为line中不包含换行符,所以我们手动地加上换行操作符。

string::size_type类型:

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

可以用auto或者decltype来推断变量的类型:

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

如果一条表达式中已经有了size()函数就不要再使用int了,这样可以避免混用int和unsighed可能带来的问题。

字面值和string对象相加:

因为标准库允许把字符字面值和字符串字面值转换成string对象,所以在需要string对象的地方就可以使用者两种字面值来代替:

string s1="hello",s2="world";
string s3=s1+','+s2+'\n';

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

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

因为某些历史原因,也为了与C兼容,所以C++语言中的字符串字面值并不是标准库类型string的对象。切记,字符串字面值与string是不同的类型。

3.处理string对象中的字符

cctype头文件中的函数
isalnum(c)  
isalpha(c)  
iscntrl(c)  
isdigit(c)  
isgrapha(c)  
islower(c)  
isprint(c)  
ispunct(c)  
isspace(c)  
isupper(c)  
isxdigit(c)  
tolower(c)  
toupper(c)  

建议:使用C++版本的C标准库头文件

C语言的头文件形如name.h,C++则将这些文件命名为cname.

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

string s("Hello World!!!");
decltype(s.size()) punct_cnt=0;
//统计s中标点符号的数量
for(auto c:s)
    if(ispunct(c))
       ++punct_cnt;
cout<<punct_cnt<<" punctuation characters in "<<s<<endl;

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

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

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

下标运算符的应用:

想要访问string对象中的单个字符有两种方式:一种是使用下标,另外一种是使用迭代器。

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

string对象的下标必须大于等于0而小于s.size();使用超出此范围的下标将引发不可预知的后果,以此类推,使用下标访问空string也会引发不可预知的结果。因此在访问指定字符之前,首先检查s是否为空。

string s("some string");
if(!s.empty())
   for(decltype(s.size()) index=0;
       index!=s.size()&&!isspace(s[index]);++index)
       s[index]=toupper(s[index]);

程序输出结果:
SOME string

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

三、标准库类型vector

标准库类型vector表示对象的集合,其中所有对象的类型都相同。

要想使用vector,必须包含头文件vector。

vector是模板而非类型,又vector生成的类型必须包含vector中元素的类型:

vector<int> ivec;
vector<Sales_item> Sales_vec;
vector<vector<string>> file;

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

1、定义和初始化vector对象

初始化vector对象的方法
vector <T> v1 v1为空vector,其中的元素是T类型的,执行默认初始化
vector <T> v2(v1) v2中包含v1所有元素的副本,两个vector对象的类型必须相同
vector<T> 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......}

列表初始化vector对象:

一,使用拷贝初始化(即使用=时),只能提供一个初始值;

二,如果提供的是一个类内初始值,则只能使用拷贝初始化或使用花括号的形式初始化;

三,如果提供的是初始元素值的列表,则只能把初始值都放在花括号里进行列表初始化,而不能放在圆括号里

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

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

如果用的是圆括号,可以说提供的值是用来构造vector对象的;

如果用的是花括号,可以表述成我们想列表初始化该vector对象,初始化过程会尽可能把括号内的值当成是元素初始值的列表来处理。

vector<int> v1(10);     //v1有10个元素,每个的值都是0
vector<int> v2{10};     //v2有一个元素,该元素的值为10

vector<int> v3(10,1);   //v3有10个元素,每个元素都是1
vector<int> v4{10,1};   //v4有2个元素,值分别是10和1

另一方面,如果初始化时使用了花括号的形式但是提供的值又不能来列表初始化,就要考虑用这样的值来构造vector对象了:

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

2、向vector对象中添加元素

push_back负责把一个值当成vector对象的尾元素“压到”(push)vector对象的“尾端(back)”:

vector<int> v2;
for(int i=0;i!=100;++i)
    v2.push_back(i);
//循环结束后v2有100个元素,值从0到99

想vector对象添加元素蕴含的编程假定:如果循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环:

在范围for语句中,预存了end()的值,一旦在序列中添加或删除元素,end函数的值就可能变得无效了。

3、其他vector操作

vector支持的操作
v.empty() 判断v是否为空
v.size() 返回v中元素的个数
v.push_back() 向v的尾端添加一个值为t的元素
v[n] 返回v中第n个位置上元素的引用
v1=v2 用v2中元素的拷贝替换v1中的元素
v1={a,b,c,d....} 用列表中元素的拷贝替换v1中的元素
v1==v2 当且仅当它们的元素数量相同且对应位置的元素值都相同,v1和v2相等
v1!=v2
<,<=,>,>= 以字典顺序进行比较

访问vector对象中元素的方法与string的方法差不多,也是通过元素在vector对象的位置。可以通过下标,也可以通过范围for语句。

size()返回vector对象中元素的个数,返回值的类型是由vector定义的size_type类型;

要使用size_type,需要首先指定它是由哪种类型定义的。vector对象的类型总是包含着元素的类型:

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

对于相等运算符和关系运算符,只有当元素的值可以比较时,vector对象才能被比较。

不能通过下标形式添加元素:

下面的代码试图为vector对象ivec添加10个元素:

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);

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

只能对确知已存在的元素执行下标操作,确保下标合法的一种有效手段就是尽可能使用范围for语句。

四、迭代器介绍

所有标准库容器都可以使用迭代器,但是其中只有少数几种才同时支持下标运算符。严格来说,string对象不属于容器类型,但是string支持很多与容器类型类似的操作。

1.使用迭代器

有迭代器的类型使用begin和end成员返回得带器。其中begin成员负责返回指向第一个元素的迭代器,end成员则负责返回指向容器(或string对象)“尾元素的下一个位置”的迭代器;end成员返回的迭代器荣成被称作尾后迭代器

//使用auto,由编译器决定b和e的类型
auto b=v.begin(),e=v.end();    //b和e的类型相同

如果容器为空,则begin和end返回的是同一个迭代器,都是尾后迭代器。

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

执行解引用的迭代器必须合法并确实指示着某个元素。试图解引用一个非法迭代器或者尾后迭代器都是未被定义的行为。

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

迭代器类型:

那些拥有迭代器的标准库类型使用iterator和const_iteraor来表示迭代器的类型:

vector<int>::iterator it;     //可读写
string::iterator it2;         //可读写

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

const_iterator和常量指针差不多,能读取但不能修改它所指的元素值;相反,iterator的对象可读可写。

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

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

C++11新标准引入了两个新函数,分别是cbegin和cend,不论vector对象(或string对象)本身是否是常量,返回值都是const_iterator:

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

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

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

(*it).empty()      //第一个圆括号必不可少

为了简化上述表达式,C++语言定义了箭头运算符(->)。箭头运算符把解引用和成员访问两个操作结合在一起,也就是it->mem和(*it).mem表达的意思相同:

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

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

已知的一个限制是不能在范围for循环中向vector对象添加元素;另外一个限制是任何一种可能改变vector对象容量的操作,比如push_back,都会使该vector对象的迭代器失效。

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

2、迭代器运算:

vector和string迭代器支持的运算
iter+n           向前移动n个元素
iter-n            向后移动n个元素
iter1+=n        将iter1加n的结果赋给iter1
iter1-=n          将iter2减n的结果赋给iter1
iter1-iter2        得到的结果是两个迭代器之间的距离
>, >=, <, <=     迭代器的关系运算符,如果某迭代器指向的容器位置在另一个                         迭代器所指所指位置之前,则说前者小于后者

所谓距离指的是右侧的迭代器向前移动多少位置就能追上左侧的迭代器,其类型是名为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=beg+(end-beg)/2;      //新的中间点
}

五、数组

数组与vector的异同点:

相似的地方是,数组也是存放类型相同的对象的容器,这些对象本身没有名字,需要通过其所在位置访问。

与vector不同的地方是,数组的大小确定不变,不能随意向数组中增加元素。

Tip:如果不清楚元素的确切个数,请使用vector

1.定义和初始化内置数组

数组的声明形如a[d],a为数组的名字,d为数组的维度。

①维度说明了数组中元素的个数,因此必须大于0;

②编译的时候维度应该是已知的,因此维度必须是一个常量表达式(字面量或常量表达式初始化的const对象)

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

和内置类型的变量一样,如果在函数内部定义了某种内置类型的数组,那么默认初始化会令数组含有未定义的值;

定义数组的时候必须指定数组的类型,不允许用auto关键字由初始值的列表推断类型;

和vector一样,数组的元素应该为对象,因此不存在引用的数组

显式初始化数组元素:

const unsigned sz=3;
int a1[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};        //错误:初始值过多

字符数组的特殊性:

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

char a1[]={'C','+','+'};        //列表初始化,没有空字符
char a2[]={'C','+','+','\0'};   //列表初始化,含有显式的空字符
char a3[]="C++";                //自动添加表示字符串结束的空字符
const char a4[6]="Daniel";      //错误:没有空间可以存放空字符

不允许拷贝和赋值:

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

理解复杂的数组声明:

我们可以定义存放指针的数组,因为数组本身就是对象,也可以定义数组的指针及数组的引用:

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

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

另外,对修饰符的数量并没有特殊限制:

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

2.访问数组元素

与标准库类型和string一样,数组的元素也能使用范围for语句或下标运算符来访问。

在使用数组下标时候,通常将其定义为size_t类型。size_t是一种机器相关的无符号类型,它可以表示内存中任意对象的大小。

在cstddef头文件中定义了size_t类型。

3.指针和数组

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

string nums[]={"one","two","three"};
string *p=&nums[0];     //p指向nums的第一个元素

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

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

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

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

decltype(ia)ia3={0,1,2,3,4,5,6,7,8,9};
ia3=p;    //错误:不能用整型指针给数组赋值
ia3[4]=i;

指针也是迭代器:

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

通过数组名字或者数组中首元素的地址都能得到指向首元素的指针;而尾后指针需要通过获取数组尾元素之后的那个并不存在的元素的地址:

int *e=&arr[10];

就像尾后迭代器一样,尾后指针也不指向具体的元素,因此不能对尾后指针执行解引用或者递增的操作。

标准库函数begin和end:

数组不是类类型,因此这两个函数不是成员函数。正确的使用形式是将数组作为它们的参数:

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

这两个函数定义在iterator头文件中;

指针运算:

迭代器的所有运算指针都可以执行。

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

如果p是空指针,允许给p加上或减去一个值为0的整型常量表达式。两个空指针也允许彼此相减,结果当然是0.

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

int ia[]={0,2,4,6,8};
int  last=*(ia+4);

下标和指针:

对数组执行下标运算其实是对指向数组元素的指针执行下标运算:

int i=ia[2];  //ia转换成指向数组首元素的指针,ia[2]得到(ia+2)所指的元素
int *p=ia;
i=*(p+2);   //等价于i=ia[2]

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

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

标准库类型限定使用的下标必须是无符号类型,而内置的下标运算无此要求。内置的下标运算符所用的索引值不是无符号类型,这一点与vector和string不一样。


4.C风格字符串

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

C风格字符串存放在字符数组中并以空字符结束,即在字符串最后一个字符后面跟一个空字符('\0')。一般利用指针来操作这些字符串。

C标准库String函数:

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

上述函数定义在cstring头文件中;

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

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

5.与旧代码的接口

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

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

string s("Hello World");

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

反过来就不成立了:如果程序的某处需要一个C风格字符串,无法直接用string对象来代替它。

使用数组初始化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));

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

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

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

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

六、多维数组

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

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

多维数组的初始化:

int ia[3][4]={     //3个元素,每个元素都是大小为4的数组
    {0,1,2,3},  
    {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}};

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

多维数组的下标引用:

如果表达式含有的下标运算符数量和数组的维度一样多,该表达式的结果将是给定类型的元素;

反之,如果表达式含有的下标运算符数量比数组的维度小,则表达式的结果将是给定索引处的一个内层数组:

//用arr的首元素为ia最后一行的最后一个元素赋值
ia[2][3]=arr[0][0][0];
int (&row)[4]=ia[1];  //把row绑定到ia的第二个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)
    for(auto &col:row){
        col=cnt;
        ++cnt;
}

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

如果不是引用类型,编译器初始化控制变量时就会自动地将这些数组形式的元素转换成指向该数组内首元素的指针,显然下一步的循环就不合法了。

指针和多维数组:

当程序使用多维数组的名字时,也会自动将其转换成指向数组首元素的指针。

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

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

上述声明中,圆括号必不可少(由内向外的分析策略):

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

在C++11新标准中,通过使用auto或者decltype就能尽可能地避免在数组前面加上一个整型指针类型了:

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

*p是一个含有4个整数的数组,像往常一样,数组名被自动地转换成指向该数组首元素的指针。

另一个版本:

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];  //等价的typedef声明

for(int_array *p=ia;p!=ia+3;++p){
    for(int *q=*p;q!=*p+4;++q)
        cout<<*q<<' ';
    cout<<endl;
}

猜你喜欢

转载自blog.csdn.net/oil_you/article/details/82149922