3.1 命名空间using声明
3.2 库类型string
3.3 库类型vector
3.4 迭代器介绍
3.5 数组
3.6 多维数组
小结
专业术语
除了第二章介绍的内置类型以外,c++还定义了丰富的抽象数据类型库。其中最重要的库类型有string,vector.
string支持可变长的字符序列,vector支持可变长的集合。
跟string和vector相关的还有一种伴随类型,叫做迭代器(iterator),它被用来存取string中的字符,或者用来存取vector中的元素。
string和vector类型由库定义,他们是内置类型数组的一种抽象。本章将会介绍数组,vector,string类型。
第二章中介绍的内置类型,直接由c++语言定义。这些类型代表了大多数计算机硬件的一种能力,如,数字和字符。标准库定义了许多更接近自然的类型,这些类型通常没有被硬件直接实现。
本章我们将介绍最重要库类型中的两个:string和vector。string是一个可变长的字符序列。而vector则保存有给定类型的个数可变的对象。本章还会介绍内置的数组类型。跟其他内置类型一样,数组也代表了硬件的一种能力。因此,数组相对于string和vector来说,使用更不方便。
在开始介绍库类型之前,首先来看看一种获取库中名字的一种简单机制。
到目前为止,在std命名空间中的名字,一旦出现在程序中都是显示的使用。例如,为了使用标准输入,写为std::cin.这个名字使用了作用域运算符(::)(1.2.8).
作用域运算符表示:让程序在左操作数的作用域内寻找右操作数对应的名字。因此,std::cin表示:使用std命名空间中的cin名字。
使用上述写法来使用库名字,比较繁琐。幸运的是,有一个更简便的方法来使用命名空间中的成员。那就是使用using声明。在18.2.2小节中将介绍另外一种方法。
using声明可以让我们不用使用namespace_name::这样子的前缀,就能使用命名空间中的名字。一个using声明有如下的格式:
using namespace::name;
一旦使用了using声明,就可以直接使用名字了。
#include
//using声明可以使用我们直接山海关cin名字。
using std::cin;
int main(){
int i=0;
cin >> i; //正确:cin就是std::cin的一个代名词
cout << i; //错误:没有使用using声明的,就必须使用权限定名
std::cout << i; //正确:显示的使用来自std命名空间中的cout
return 0;
}
每个名字都需要分开使用using声明
#include
//using 声明来自标准库中的名字
using std::cin;
using std::cout;using std::endl;
int main(){
cout << “Enter two number:”<< end;
int v1,v2;
cint >> v1 >> v2;
cout << “The sum of ”<< v1<<” and”<< v2
<<” is ”<< v1+v2<
cin,cout,endl的using声明表示,可以直接使用名字,而不用加std::前缀。c++程序格式比较自由,因此可以将每个using语句单独成行,也可以将多个using语句放在同一行中。关键是:对于每个名字,都必须要有自己的using声明,每个声明的结尾必须是分号。
头文件不应该包含using声明
头文件不应该使用using声明。因为,头文件会被复制到包含它的源文件中,如果使用了using声明,也会被包含在源文件中。而某些源文件在不经意间,包含了头文件中的using声明,但是它并不想使用命名空间中的名字,这会造成命名的冲突。
注意
从这里开始,我们编写的例子,将假定标准库中的名字已经使用了using声明,因此,我们将直接使用cin,而不会是std::cin。
同时,为了保证代码的精简,并不会给出using声明语句,和#include指令。附录A的表A.1列出了本书中使用的名字和对应的标准库头文件。
警告 读者在编译本书例子的时候,应该总是有意识的增加相应的#include指令和using声明。
string是一个可变长的字符序列.为了使用string类型,必须包含string头文件.因为是标准库中的一部分,所以string被定义在了std命名空间中.下面给出的所有例子,都假定有如下的代码:
#include
using std::string;
本章将介绍常用的string操作,9.5小节将介绍另外一部分操作.
注意 类型除了标准库指定的操作以外,还强加了性能上面的考虑.因此,标准库类型对于常用的操作具有足够高的性能.
类决定了对象怎么被初始化.类可以定义不同的方式来初始化它的对象.每种初始化方式必须要能有所区别,或者初始值个数不同,或者初始值类型不同.表3.1列出了常见初始化string的方式.下面是一些例子:
string s1; //默认初始化,s1 是空字符串
string s2 = s1; //s2是s1的一个拷贝
string s3 = “hiya”; //s3是一个字符串字面量的拷贝
string s4(10,’c’); //s4 为cccccccccc
默认初始化一个字符串,将会产生一个空的字符串,即,这个字符串没有任何字符.当直接使用一个字符串字面量的时候,这个字面量将会复制到新创建的string对象中,但是不会复制字符串字面量最后的空字符.当使用一个数字和一个字符的时候,新的string对象就保存着给定数字这么多的连续重复的字符.
直接初始化和拷贝初始化
在2.2.1小节中讲到c++有几种不同的初始化方式.通过使用string类型,可以帮助我们理解这些方式的异同.
当使用=初始化一个变量的时候,相当于请求编译器复制右边的初始值到左边新创建的对象中,这就是拷贝初始化.当没有使用=时,就是使用的直接初始化.
当使用单个初始值的时,可以使用拷贝初始化或者直接初始化.当多个初始值时,必须使用直接初始化,例如上例中的s4.
string s5 = “hiya”;//拷贝初始化
string s6 (“hiya”);//直接初始化
string s7(10,’c’); //直接初始化,s7 cccccccccc
当使用多个初始值的时候,可以通过创建一个临时对象,然后再使用拷贝初始化
string s8 = string(10,’c’); //拷贝初始化:s8为cccccccccc
s8的初始值为string(10,’c’),这个创建了一个给定大小和字符的string对象.然后再将这个对象的值复制到s8中.就相当于相面的形式一样;
string temp(10,’c’); //temp为cccccccccc
string s8 = temp; //拷贝temp到s8中
虽然初始化s8的代码是合法的,但是跟s7比较起来,可读性更差,并且还没有其他优势.
类除了定义对象的创建和初始化,还定义了对象可以执行的操作.类可以定义通过名字来调用的操作,还可以定义像<<,>>,+的操作符号.表3.2列出了常用的string的操作
读写字符串
在第一章中,使用了iostream库来读写内置类型,例如int,double等等.也可以使用同样的IO操作来读写string:
//注意:#include和using声明必须在编译之前添加上
int main(){
string s; //空字符串
cin >> s; //读取字符串到s中,遇到空白停止
cout << s << end; //将s写入output中
return 0;
}
程序首先定义一个空的字符串s,下一行通过标准输入将数据读入s中.字符串的输入运算符读取并且丢弃开头的空白字符(如,空格,换行,tab),一直读字符,直到遇到下一个空白字符.
因此,如果输入”空格空格 hello world! 空格空格”(注意开头,和结尾的空格).那么输出只有hello,并且前后没有空格.
跟内置类型的输入输出操作一样,string操作数返回它的左操作数作为结果.因此我们可以将多个读写操作链接起来.
string s1,s2;
cin>>s1>>s2; //降低一个保存在s1中,第二个保存在s2中
cout << s1 << s2 << endl;//输出两个字符串
如果我们给定如下的输入:”hello world!”.输出为HelloWorld!
读未知数量的字符串
在1.4.3小节中,写了一个读取未知数量的int的程序。现在可以写一个类似的程序用于读取string。
int main(){
string word;
while(cin >> word){ //读取直到遇到文件末尾
cout<< word << endl; //每一个单词后面跟一个换行
}
return 0;
}
这个程序,读取string,而不是int类型。然而,这个while的条件表达式跟前面的程序类似。条件表达式在读取完成之后,判断输入流的状态。如果输入流是合法的(没有遇到文件末尾或者非法输入),则while的循环体将会被执行。在循环体中,输出输入的字符。一旦遇到文件结束符(或者非法输入)执行将跳出while的循环体,到达循环体下面的语句。
使用getline读取整行
有时不想忽略输入的空白字符,此时我们可以使用getline函数代替>>运算符。getline函数接收一个输入流和一个字符串对象,通过读取给定的输入流,直到遇到换行符,将读取的内容保存在给定的字符串对象中。(注意,并不会保存最后的换行符)getline函数一遇到换行符,即使这个换行符是输入中的第一个字符,它也会马上停止读取,并马上返回。如果在输入的第一字符是换行符,那么返回的结果将是一个空的字符串。
跟输入运算符一样,getline返回他的输入流参数。因此我们可以使用getline作为条件表达式。例如,可以重写上面的程序,让它一次输出一行,而不是每个单词一行。
int maint(){
string line;
//一次读取一行,直到遇到文件结束符
while(getline(cin,line))
cout << line << end;
return 0;
}
因为,line没有包换换行符,因此需要手动写一个。通常使用endl,结束当前行,并刷新缓冲区。
注意: 通过getline读取到的换行符被丢弃,因此读取到的换行符并没有保存在字符串中。
string的empyt和size操作
empty函数返回预期的结果:返回一个bool类型,用于表明这个字符串是否为空。跟Sales_item的isbn成员一样,empty是string的成员函数。使用点号运算符,来表明,我们想在那个string对象上面操作empty函数。
我们可以修改上面的程序,让其只打印非空的行。
while(getline(cin,line)){
if(!line.empty())
cout << line << endl;
}
条件表达式,使用了逻辑非运算符。这个运算符返回操作数相反的bool值。此处,当str不是空的时候,条件表达式返回true。
size成员函数返回一个string对象的长度。(即,在字符串里面的字符数量)。我们可以使用size让程序只打印字符数超过80个的行:
string line;
while(getline(cin,line))
if(line.size() > 80)
cout << lin << endl;
string::size_type 类型
size返回一个int类型,或者unsigned int 类型是符合逻辑的。但是size函数返回了一个string::size_type类型。
string库类型包括其他的库类型,定义了几种配套的类型。配套类型使得库类型的使用与机器无关。size_type是这些配套类型中的一种。为了使用string类中中定义的size_type,使用作用域运算符,来表示size_type定义在string类内。
尽管我们不知道string::size_type的精确的类型。但是我们知道,他是一个无符号类型,并且有足够保存任何string类型的大小。任何用来存储string大小的变量的类型,应该被定义成string::size_type.
诚然,使用类型string::type_size是非常繁琐的。在c++11新标准下,可以使用auto和decltype让编译器自动推导出正确的类型。
auto len = line.size(); //len的类型为string::size_type
因为size返回一个无符号的类型,所以,谨记混合使用有符号和无符号可能导致不期望的结果产生。例如n是一个有符号的,并且保存有一个负数,则s.size() < n 几乎可以确定为true。因为保存n中的值将会隐式转换成一个无符号的大数。
提示:
在size()中不要使用有符号数,可以避免这种无符号和有符号之间产生的问题。
比较string
string定义了几个用来比较string的运算符。这些运算符通过比较字符到达比较的效果。比较运算是大小写敏感的,即字符的大写和小写是两个不同的字符。
相等运算符(==,!=)判断两个字符串是否相等,或者不等。如果两个字符串长度相同,并且包含相同的字符,那么这两个字符串相等。关系运算符<,<=,>,>=判断一个字符串是否小于,小于等于,大于,大于等于另外一个字符串。这些关系运算符使用的策略跟字典相同。
• 如果两个字符串长度不同,并且短字符串中的字符与长字符串中的字符,一一相等,那么短的字符串小于长的字符串
• 如果两个字符串中对应位置的字符不相同,那么两个字符串比较的结果就是,比较第一对不相同的字符。
举个例子,思考下面的字符串:
string str = “Hello”;
string phrase= “Hello World”;
string slang = “Hiya”;
按照规则1,str是小于phrase的,按照规则2,slang是大于str和phrase。
字符串的赋值
通常情况下,尽量让库类型的使用与内置类型一样简便易用。因此大多数的库类型都支持赋值操作。对于string类型,允许将一个string对象赋值给另外一个字符串对象。
string str(10,’c’),str; //st1 cccccccccc,st2为一个空字符串
st1 = st2;//赋值:用st2的内容,代替st1的内容。现在st1和st2都是空字符串了
两个字符串相加
相加两个字符串,会得到一个新的字符串,它由左操作数的字符串和右操作数的字符串连接起来组成。即,当两个字符串使用加号(+)运算符的时候,结果产生一个新的字符串,新字符串的字符,由左边的字符串跟上右边的字符串组成。复合赋值运算符(+=),将右边的字符串连接到左边的字符串中。
string s1 = “hello ,”,s2= “world\n”;
string s3 = s1+s2; //s3 为hello, world\n
s1 += s2; //等价于s1 = s1+s2
字面量与字符串对象相加
正如2.1.2小结所示,可以使用一种不是期望类型的类型,前提是,这种类型能够转换成期望类型。string库类型允许,将字符字面量和字符串字面量转换成string对象。
因为在期望string对象的地方,我们可以使用这些字面量,所以,上述程序可以写成如下形式:
string s1 = “hello”,s2 = “world”;//在s1中和s2中没有标点符号
string s3 = s1 + “, ”+s2 +’\n’;
当我们混合使用string对象和字符、字符串字面量的时候,至少要保证加号运算符的一边为string对象。
string s4 = s1 + “, ”; //正确:string对象和字面量相加
string s5 = “hello” + “, ” //错误:没有string对象
string s6 = s1 + “, ”+”world”; //正确:每一个+都有一个string对象
string s7 = “hello” +”, ”+s2; //错误:第一个+没有一个string对象
s4和s5的初始化,仅涉及单个运算符,所以很容易判断初始化是否合法。s6的初始化,可能让人有点迷惑,不过他的工作原理和输入输出的链式使用是一样的。这个初始化,可以按如下分个组:
string s6 = (s1 + “, ”) + “world”;
子表达式s1+“, ”
返回一个字符串对象,它作为第二个+运算符的左操作数。等价于下面的写法:
string tmp = s1 + “, ”;
s6 = tmp + “world”;
另一方面,s7的初始化是错误的,加上括号之后,如下:
string s7 = (“hello” + “, ”)+s2;//错误,不能将两个字面量直接相加
现在容易看出,第一个子表达式将两个字符串字面量相加,c++并没有定义这种方式,因此这个表达式是错误的。
警告
因为历史原因,和c的兼容性原因,字符串字面量不是标准string类型。因此记住,这些字面量与string类型是不同的。
我们经常需要处理字符串中的单个字符。我们想检查字符串中是否还有空白,或者将字符改成小写,或者查看某个子字符串是否存在等等。
这些操作的一部分就是:我们怎么获取字符串中的字符,有时,我们需要处理每一个字符,有时又只需要处理其中的一部分字符,有时遇到某个条件就停止操作。经验证明这些操作涉及语言和库的很多方面。
处理字符或者改变字符的特性,在以前是做过这种操作的。这种操作通过下面表格提供的库函数来完成。这些函数被定义在了cctype头文件中
建议:使用c++版本的c标准库文件
除了为c++定义库文件以外,c++标准库还兼容了c库。形如name.h这种形式的c库头文件,对应的c++版本被命名为cname——移除了.h后缀并且将字符c放在了名字的前面。字符c表示:这个头文件是c库中的一部分。
因此,cctype跟ctype.h文件有相同的内容,但是前者更符合c++程序。事实上,定义为cname的头文件,被定义在了std命名空间中,而.h头文件,则没有。
通常,c++程序更因该使用cname版本的头文件,而不是.h的头文件。通过这种方式,标准库中的名字,更容易在std命名空间中找到。使用.h版本的头文件,将部分程序负担放在了程序员身上,他们需要记住,那些名字继承于c头文件,那些是c++中独有的。
使用基于范围的for语句,处理每一个字符
如果我们想对字符串中的每个字符做操作,到目前位置,最好的办法就是,使用c++新标准中的范围for语句。这种语句遍历给定序列中的每一个元素,然后在每个元素上面执行一些操作。语法如下:
for (声明语句:表达式)
语句
此处,表达式表示某种类型的对象,它代表了一个序列,声明语句定义了一个变量,这个变量将会用来存取序列中的元素。
在每一次遍历中,声明语句中定义的变量,他的初始值来自于表达式中的下一个元素。
一个字符串对象代表了一个字符序列。因此可以使用字符串对象作为范围for语句的表达式。作为例子,我们可以使用范围for语句打印字符串中的每个字符在单独一行中:
string str(“some string”);
//将str字符串中的字符单独打印一行
for(auto c:str)
cout << c <
for循环将变量c和str联系起来。在for里面定义循环控制变量的方式和定义普通变量的方式一样。在此处,使用了auto关键字让编译器决定c变量的类型,这里为char类型。在每次遍历的时候,在str中的下一个字符,将会复制到c变量中。因此,我们可以读这个循环为:“对于每一个在str中的字符c”做某某操作。在此处,某某操作,就是打印字符,并且每个打印字符后面跟一个换行符。
写一个更加复杂的例子:我们将使用范围for语句和ispunct函数来计算在字符串对象中的标点符号:
string s(“hello world!!!”);
//punct_cnt 跟s.size的返回值有相同的类型,见2.5.3小节
decltype(s.size()) punct_cnt = 0;
//计算在s中的标点符号的个数
for(auto c: s){
if(ispunct(c))
++punct_cnt;
}
cout << punct_cnt << “ punctuation charactes in ” << s << endl;
这个程序的输出是:
3 punctuation characters in Hello World!!!
这里我们使用decltype声明我们的计数器,punct_cnt,它的类型跟调用s.size()的返回值类型相同,为string::size_type。我们使用了一个范围for语句,处理每一个在字符,然后检查每一个字符是否为标点符号,如果是,就使用自增运算符,将计数器加一。当范围for语句处理完成,就打印结果。
使用范围for语句改变在字符串中的字符
如果我们想改变字符串对象中字符,必须定义循环变量为一个引用类型。谨记,引用只是一个对象的另外一个名字。当我们使用引用作为我们的控制变量时,这个变量依次被绑定到字符串对象中的每个字符。使用这个引用,可以改变这个引用绑定的字符。
新例子不再计算标点符号的个数,而是将字符串对象中的所有字符都变成大写字符。为了达到这个效果,可以使用标准库中的toupper函数,它有一个字符类型的参数,然后将这个参数转变成这个字符对应的大写形式。为了转换整个字符串,需要每一个字符都调用toupper函数,然后将结果放回同一个字符中。
string s(“Hello world!!!”);
//转换s为大写
for(auto &c:s)
c= toupper(c);
cout << s << end;
这段代码的输出为:
HELLO WORLD!!!
在每一次的遍历中,c绑定到了s中的下一个字符。当我们赋值给c的时候,相当于改变了s中的字符。因此,使用下面的代码:
c = toupper(c);//c是一个引用,因此赋值改变了s中的字符
这个语句改变了s中的字符,当for语句执行完成,s中的所有字符就变成了大写的了。
仅处理部分字符
当需要处理字符串中的每一个字符的时候,范围for语句可以非常方便的使用。但是,有些时候,我们需要处理单个字符,或者当条件满足的时候,存取字符操作就要停下来。例如,只对字符串中的第一个字符,或者第一个单词,进行大写化。
这里有两种方法,可以分开的访问字符串中的字符:可以使用下标或者使用迭代器。在3.4小节和第九章,将会介绍更多关于迭代器的知识。
下标运算符([ ])接受一个string::size_type类型的参数,这个参数表明你想访问字符串对象中字符对应的位置。这个操作符返回一个指向了给定位置的引用
字符串对象的下标从0开始;如果s是至少有两个字符的字符串。s[0]表示第一个字符,s[1]是第二个,最后一个字符串为s[s.size()-1]
注意:使用在下标中的值,必须大于等于0小于size()
使用超出这个范围的索引值,造成的结果是未知的。
意味着,在一个空字符串中进行一个下标操作也是未知的。
在下标中的值被称为“下标”或者“索引”.索引可以是任何能够产生整数值的表达式。但是,当索引值是有符号的时候
,会被自动的转换成string::size_type代表的无符号类型.
下面的例子,使用下标运算符,打印字符串对象中的第一个字符:
if(!s.empty()) //保证s中有字符可以被打印
cout << s[0] << endl; //打印第一个字符
在获取字符之前,我们检查了s是否为空。在任何使用下标的情况,都需要保证给定位置有值。如果s是空字符串,那么s[0]是未知的。
只要字符串不是const,可以给下标运算符返回的字符赋一个新的值。例如,我们可以让第一字符大写,如下:
string s (“some string”);
if(!s.empty())
s[0] = toupper(s[0]);
这段代码的输出为:
Some string
使用下标进行遍历
再来个例子,将s中保存的第一个单词变为大写的:
for(decltype(s.size()) index = 0;
index != s.size() && !isspace(s[index]); ++index)
s[index] = toupper(s[index]);
程序输出为:
SOME string
在我们的循环中,使用index作为下标。使用decltype给index一个正确的类型。给index初始化为0,是为了从s中的第一个字符开始访问。在每次遍历中,自增index的值,用来获取s中的下一个字符。在循环体中,将当前的字符大写化。
在for语句中的新的部分是条件表达式。这个条件表达式,使用逻辑与运算符(&&)。只有当两个操作数都为true的时候,这个运算符才会返回true。对于这个运算符我们需要记住的是,只有当左边操作数为true的时候,才会去计算右边操作数的值。在此处,我们保证下标在正常的范围内。即,s[index]只有在index不等于s.size()的时候才会被执行。因为index永远不会超出s.size()的值,所以index永远小于s.size().
注意:编译器不检查下标
当我们使用下标的时候,我们必须保证下标在正常的范围内。即,下标必须大于等于0并且小于size().简化代码的一种方式是:作为下标的类型为string::size_type。因为这个类型是无符号的,可以保证至少大于零。当我们使用了size_type作为下标之后,只需要检查下标小于size()即可
警告 标准库不要求检查下标的合法性。使用超出范围的下标结果是未知的。
使用下标进行随机的存取
在前面的例子中,我们通过每次加一,去大写化字符。我们也可以通过计算得到某一个下标然后,直接获取下标对应位置的字符。没必要每次都按照顺序进行读取。
作为例子,我们假定有0到15的某个数字,并且想将其转换成对应的十六进制。用一个字符串保存有16个十六进制的各个数字。如下:
const string hexdigits = “0123456789ABCDEF”;
cout << “Enter a seriers of numbmbers between 0 and 15”
<<” separated by spaces.Hit ENTER when finished:”<> n)
if(n
如果给如下的输入:
12 0 5 15 8 15
输出如下:
Your hex number is: C05F8F
通过初始化hexdigits开始程序,并且将hexdigits作为字符串常量,因为我们不希望改变这个变量的值。在循环的内部,使用了n作为hexdigits的下标。hexdigits[n]代表了hexdigits在n位置的字符。例如,当n为15的时候,结果就是F。为12的时候,就是c。依次类推。最后我们将结果添加到result里面,然后将结果一次性打印出来。
无论何时,当时用下标的时候,我们应该思考,这个下表是否在正确的范围内。在这个程序中,n是string::size_type类型,他是无符号类型的,因此可以保证它大于等于零。在使用n作为下标之前,我们还必须保证n是小于hexdigits的长度。
一个vector是对象的集合,这些对象具有相同的类型。每一个对象都有一个索引值,用于存取这个对象。一个vector对象经常被当作一个容器,因为它包含其他的对象。在第二部分,将会介绍更多的容器相关的知识。
为了使用vector,必须包含相应的头文件。在后面的例子中,假定都有下面的代码:
#include
using std::vector;
c++有类模板和函数模板,vector是一个类模板。写一个模板需要对c++有相当深入的理解。事实上,在十六章之前,我们都不需要明白怎么创建一个模板,但是我们可以使用这些模板。
模板本身不是函数或者类。但是,他们可以被认为是一些列的指令,这些指令可以让编译器自动生成类和函数。编译器根据模板创建函数和类的过程称为实例化。当使用模板的时候,需要指定编译器实例化这个模板为那种类型的类或者函数。
对于一个类模板来说,我们通过给额外的信息,让其知道被实例化成那种类,需要提供哪些信息,取决于模板。怎么给定需要的信息,操作都是相同的:将信息放在模板名后面的一对尖括号之间。
对于vector来说,他需要的额外信息,是vector保存的对象的类型:
vector ivec; //ivec 保存int类型的对象。
vector Sales_vec: //保存Sales_items
vector> file: //vector的元素为vector
在这个例子中,编译器会生成三个不同的类型:
vector,vector,vector>.
注意:
vector是一个模板,不是类型。根据vector生成的类型,必须包含元素类型,例如vector
我们可以定义保存大部分类型的vector。因为引用不是对象,因此没有保存引用的vector。
我们可以定义除引用之外的内置类型和类类型的vector。事实上,还可以定义一个vector,它的元素也为vector。
值得注意的是:在c++的早期版本中,使用了另外一种语法来定义元素是vector的vector。在老版本中,不得不在元素类型和右尖括号之间加一个空格,——vector
而不是vector
警告:
对于元素是vector的vector来说,一些老版本的编译器需要使用老版本的语法。
和其他的类型一样,vector模板决定了它自身的定义和初始化。表3.4列出了vector定义的常见的初始化方式。
使用下面的代码可以默认初始化一个vector,它创建一个指定类型的空vector。
vector svec; //默认初始化,svec没有任何元素
初始化一个空的vector似乎用处不大,但是,马上就介绍,可以在运行的时候,向vector中添加元素。事实上,对于vector的常见操作,正是,先定义一个空的vector,然后在运行的时候,向其中添加元素。
当定义一个vector的时候,也可以使用初始化值。例如,可以复制另外一个vector中的值。当我们复制一个vector的时候,新的vector中的每一个元素,都是老的vector中对应元素的一个拷贝。当然,这两个vector必须是具有相同的类型:
vector ivec; //初始化为空
//给ivec一些值
vector ivec2(ivec); //复制ivec到ivec2
vector ivec3 = ivec; //复制ivec到ivec3
vector svec(ivec2); //错误:svec保存的元素类型为string,而不是int
列表初始化一个vector
提供元素值的另外一种方式是:使用c++11新标准的列表初始化,在一对大括号之间列出零个或者多个初始值。
vector articles= {“a”,”an”,”the”};
vector有三个元素,第一元素为字符串a,第二个为字符串an,第三个为字符串the。在大多数场景,不是所有场景,我们可以互换地使用这些初始化方式。但是,迄今为止,已经见过了两种例外的初始化例子了:第一,使用复制初始化(使用=)的时候,只能指定一个初始值。第二,当使用类内初始值(2.6.1)的时候,必须使用复制初始化,或者使用大括号初始化。第三个限制是,使用列表初始化的时候,只能使用大括号,不能使用小括号。
vector v1{“a”,”an”,”the”}; //列表初始化
vector v2(“a”,”an”,”the”); //错误
创建指定数量的元素
我们还可以根据一个数量和一个元素值来创建一个vector。数量决定这个vector有多少个元素。一个元素值决定了这个vector的每个元素的初始值。
vector ivec(10,-1); //十个int元素,每一个元素都是-1
vector svec(10,”hi!”); //十个string,每一个都是“hi!”
值初始化
通常可以省略值,而只指定元素个数。此种情况下,标准库会自动创建初始值。这个初始值取决于vector中保存的元素类型。
如果vector保存内置类型,比如int,元素被初始化为0.如果元素是类类型,例如,string,元素的初始值就是string的默认初始值。
vector ivec(10); //十个元素,每个元素为0
vector svec(10); //十个元素,每个元素都为空字符串
这种初始化,有两个限制:第一,有些类需要显式的提供初始值。如果vector中保存的对象不支持默认初始化,那么就必须提供一个初始的元素值。因此,通过只指定元素个数的方法来创建vector是不可能的。
• 当只使用元素个数来创建vector的时候,我们必须使用直接初始化的格式。
vector vi = 10; //错误:必须直接初始化来指定大小
此处,我们使用了10,为了创建一个含有十个int元素的vector,而不是将10复制到vector中。因此不能使用复制初始化。在7.5.4中将会看到更多这种限制。
列表初始化还是元素个数
在一些情况下,初始值的真实含义取决于我们传递的初始值使用的是大括号,还是小括号。例如,当使用一个int值来初始化一个vector
vector v1(10); //v1有十个元素,每个元素值为0
vector v2{10}; //v2 有一个元素,值为10
vector v3(10,1); //v3有十个元素,每个元素值为0
vector v4{10,1}; //v4有两个元素,分别为10和1
当我们使用小括号的时候,可以说,提供的值用来构造对象。因此,v1使用初始值作为大小,v3使用初始值作为大小和元素值。
当我们使用大括号的时候,可以说,如果可能,我们想列表初始化对象。就是说,如果能够将大括号内的值当作元素初始值的列表的话,那么就会按照这种方式来解释这种语句。只有当不能作为列表初始化的时候,才会考虑其他的初始化方式。给v2和v4的初始值被用作元素。这两个对象被分别初始化为一个和两个元素。
另一方面,如果使用大括号并且无法进行列表初始化的时候,这些值将被用来构造对象。例如,为了列表初始化一个string类型的vector,必须给出作为string的值。在此例中,给定值是列表初始化还是构造对象,并不会引起困惑:
vector v5{“hi”}; //列表初始化,v5有一个元素
vector v6(“hi”); //错误,不能够通过一个string来构造一个vector
vector v7{10}; //v7有十个默认的初始值元素
vector v8{10,”hi”}; //v8有十个“hi”
在这些定义中除了一个以外其他都是使用的大括号,但是只有v5是列表初始化。为了使用列表初始化,大括号内的值的类型必须和vector中的元素类型相匹配。因为不能使用int去初始化一个string类型,所以对于v7和v8的初始化不能被当作元素的初始值。如果不能使用列表初始化,那么编译器就会寻找其他的方式把给定的值用来初始化对象。
当vector的初始值个数比较少的时候,或者当vector是复制另外一个vector的时候,或者想所有元素的值相同的时候,直接初始化一个vector是比较方便的.
然而大多数情况下,我们不知道vector需要多少元素,或者不知道这些元素的值,就算知道了元素的值,但是元素的个数可能非常多,那么在创建vector的时候,直接初始化就特别的麻烦.
例如,如果我们需要一个0-9的vector,这是非常容易创建的,可以使用列表初始化.但是如果我们需要的是0-99或者0-9999呢?列表初始化就显得非常的笨拙.在这种情况下,最好是创建一个空的vector然后使用vector的成员函数push_back实时添加元素到vector中.
push_back将一个值”pushes”到这个vector的最后一个元素的后面.例如:
vector v2; //空的vector
for(int i=0;i!=100;++i)
v2.push_back(); //添加整数序列到v2中
//循环结束,v2有100个元素,0-99
尽管知道需要100个元素,但是还是定义了一个空的v2.每次迭代将一个新的整数值添加到v2中.
当只有在运行的时候,才能知道有多少元素,此种情况下,也可以使用相同的方法创建空vector然后实时添加元素.例如,从输入中读入值,然后将其存入vector中:
//从标准输入中读入单词,然后将其保存在一个vector中
string word;
vector text; //空的vector
while(cin >> word){
text.push_back(word); //添加word到text中
}
这个例子中,再次以一个空的vector开始,然后,读取并存储一个未知数量的值到text中.
关键概念:vector可以高效的增长
c++标准要求,vector的实现必须能够高效的实时添加元素.因为vector能够高效的扩增,因此没必要为了性能,首先定义一个固定大小的vector.当然如果所有的元素值都相同,完全可以先定义一个有大小的vector.如果元素的值不同,通常就是先定义一个vector然后在运行的时候向其中添加元素,如9.4小节.vector提供了运行时添加元素的能力.
以一个空的vector开始,然后在运行时添加元素.这种方式于c内置类型的时候,以及其他语言有点不同.事实上,如果你经常使用c或者java,你可能希望有一个固定的大小定义vector,然而事实往往相反.
编程实现向vector中增加元素
我们可以高效的向vector中添加元素,简化了很大部分的编程.但是这种简化,也对程序提出了新的要求:我们必须保证当vector中的大小改变的时候,循环不能出错.
随着对vector使用的深入,会有更多的隐含意义被揭露.但是此时,有一个隐含规则需要注意:当向vector中动态的添加元素的时候,不能够使用范围for语句.
警告 对于范围for语句来说,不能在循环体内改变序列的大小.
除了push_back以外,vector还提供了几个其他的操作.他们大都跟string上面对应的操作类似.表3.5列出了部分比较重要的方法
存取vector中的元素,与存取string中的字符类似.例如,我们可以使用范围for语句来操作vector中的所有元素:
vector v{1,2,3,4,5,6,7,8,9};
for(auto &i:v)
i*i=i;
for (auto I:v)
cout << I << “ ”
cout << endl;
第一个循环中,我们定义了控制变量i,它是一个引用,因此可以通过i来赋值新值给v.同时,使用auto让编译器决定i的类型.这个循环使用了一个新的复合赋值运算符(1.4.1).正如+=将右边操作数加到左边操作数上,然后将结果存储在左边操作数里面.*=将右边操作数乘以左边操作数,然后将结果存储在左边操作数上.第二个for语句打印每个元素.
empty和size成员函数跟string对应的成员函数相同:empty返回一个布尔值,这个布尔值表示vector是否有元素.size返回vector中的元素个数.size返回的类型为vector<*>::size_type.
注意: 为了使用size_type,必须命名vector需要的类型.一个vector类型总是包含它的元素类型.
vector
::size_type //正确 vector::size_type //错误
相等和关系运算符与string对应的运算符类型作用类似.如果两个vector有相同的元素个数,并且对应位置的元素相同,那么这个两个vector就相等.关系运算符使用字典序列进行大小的比较:如果vector大小不同,但是元素在相应位置是相同的,那么短一点的vector小于长一点的vector.如果两个vector中的元素都不相同,那么他们的大小,取决于第一个不同的元素的大小.
只有当元素类型可以比较的时候,才能够比较vector.一些类,比如string定义了相等和关系运算符,他们可以进行vector的比较.另外一些类,如Sales_item类.他们就不能比较.
Sales_item类支持的操作,已经被列在了1.5.1小节中.这些操作不包括相等和关系运算符.因此不能比较两个vector
计算vector对象的索引
通过下标运算符可以获取某个元素.跟string一样,下标从0开始,类型为size_type.当vector不是const时,可以通过下标修改vector中的元素.因此,照着3.2.3小节中的例子,可以计算一个index然后直接获取在该处的元素.
举个例子:假定有一个分数的集合,这个集合包含0到100的分数.现在想计算各个分段的成绩情况,每个分段以10为界限.从0到100有101个可能的分数.这些分数可以用十一个分段表示:每个分段有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分段一个,40分段一个,50分段没有,60分段2个,70分段三个,80分段2个,90分段4个,100分段1个.
我们使用含有十一个元素的vector来保存各个分数段中的个数.某个分数通过除以10来决定各个分数段的索引值.两个整数相除,还是整数,小数部分被忽略.例如,42/10=4,65/10=6,
100/10=10.一旦得到了分数段的索引值,就可以使用下标运算符获取vector中的数据,然后做相应的增加.
vector scores(11,0);
unsigned grade;
while(cin >> grade){
if(grade < = 100)
++scores[grade/10];
}
先定义一个vector来保存每个分数段的个数.此例中,每个元素具有相同的值,所以我们定义了11个元素,每个元素的初始值为0.while的条件表达式读取标准输入中的分数.循环体中保证读取到的值在有效范围内.如果有效,就将vector中的相应位置自增.
执行自增的那条语句,是c++程序保持简介的一个很好例子.
++scores[grade/10]; //对当前的分数段,执行自增.
等价于
auto ind = grade/10; //获取索引
scores[ind] = scores[ind] + 1; //加一
通过除以10来计算分数段的索引值,然后使用这个索引值来存取scores.将scores相应的元素加一,表示有一个分数在这个分数段中.
正如所见,当我们使用下标的时候,我们应该总是考虑下标不会超出范围.在这个程序中,通过让输入在0到100,来保证下标不会超出0到10的范围.下标的范围在0到scores.size()-1之间.
下标不能用来添加元素
c++新手可能有时认为可以通过下标向vector中添加元素.然而这是不被允许的.下面的程序想要增加是个元素到ivec中;
vector ivec; //空的vector
for(decltype(ivec.size()) ix = 0;ix!=10;++ix){
ivec[ix] = ix; //致命错误:ivec没有元素
}
这是错误的: ivec是空的vector.没有元素能够被下标运算符操作.正确的方法是使用push_back向vector中添加元素.
for(decltype(ivec.size()) ix = 0;ix!=10;++ix){
ivec.push_back(ix); //正确,增加一个新的元素到ix中
}
警告: 下标运算符是去获取vector中已经存在的元素,并不会增加一个新的元素(有别于一些脚本语言)
注意:下标只能是已经存在的元素 谨记下标运算符只能作用域已经存在的元素.例如:
vector
ivec; //空的vector cout << ivec[0]; //错误:ivec没有元素可用 vector ivec2(10); //vector有十个元素 cout << ivec2[10] ; //错误:ivec2的元素下标为0到9 下标运算一个不存在的元素是错误的编程,同时编译器也不能检测这种下标越界的错误,这个只有在运行的时候得到一个未知的值.
下标运算一个不存在的元素,是一种极其严重的,常见的错误。被称作缓冲区溢出错误就是这种错误造成的。这也是pc以及其他设备上面,应用程序出现安全漏洞的原因。
提示:保证下标合法的一种好的方式是:尽量使用范围for语句。
尽管我们可以使用下标访问string中的字符,或者vector中的元素。但是还有一种更加常用的方法达到相同的目的——迭代器(Iterator).在第二部分,将会学到,除了vector以外,标准库还定义了几个其他常见的容器。这些标准库的所有容器都支持使用迭代器,但是这些容器只有少数支持下标。从技术上来讲,string不是容器类型,但是string支持容器的大部分操作。跟string一样,vector支持下标操作。跟vector一样,string支持迭代器操作。
跟指针一样(2.3.2小节),迭代器可以间接的访问对象,此种情况下,这个对象是一个容器的元素,或者是string中的字符。迭代器可以用来访问元素,还可以用来移动元素。跟指针一样,迭代器也有合法和非法之分,合法的迭代器既可以用来表示一个元素,还可以用来表示容器最后一个元素之后的位置,其他的情况就属于非法迭代器。
跟指针不同的是,不使用取地址符去获取一个迭代器,而是使用返回迭代器类型的成员函数。这些具有迭代器的类型,通常有begin和end两个成员函数。begin返回第一个表示元素(或者字符)的迭代器。如:
//编译器决定了b和e的类型,见2.5.2
//b表示第一个元素,e表示v最后一个元素后面的位置。
auto b =v.begin(),e=v.end();//b和e有相同的类型
end返回的迭代器指向了容器最后一个元素后面的位置。该迭代器指向了一个不存在的元素,常用来表示已经处理完了所有元素的一个标志。end返回的迭代器经常被称作“尾后迭代器”或者缩写为“尾迭代器”。如果容器是空的,那么begin和end返回的迭代器相同。
注意: 如果容器为空,begin和end返回的迭代器相等,他们都是尾后迭代器。
通常,我们不知道一个迭代器具体的类型。此处例子中,使用了auto来让编译器自动推导b和e的类型。因此,这些变量的类型,分别有begin和end成员函数的返回值决定。在108页(原书)将会介绍更多关于这种类型的知识
迭代器的操作
迭代器仅支持几种操作,他们被列在了表3.6中。可以使用==和!=来比较两个迭代器,如果这两个迭代器是同一个元素或者都是尾后元素,那么两个迭代器相等,反之则不等。
跟指针一样,将一个表示元素的迭代器进行解引用操作,即可以得到这个元素。同样的,只能解引用一个合法的迭代器,如果解引用一个非法的引用或者一个尾后迭代器,那么得到的值是未知的。
例如,重写3.2.3中的程序,将string的第一个字符变成大写,这次使用迭代器来代替以前的下标运算。
string s(“some string”);
if(s.begin() != s.end()){ //保证s非空
auto it = s.begin(); //获取s的第一元素
*it = toupper(*it); //将s变成大写
}
跟以前的程序一样,首先检查s为非空。在这个程序中,通过比较begin和end返回的迭代器来达到目的。如果string为空,那么这些迭代器会相等。如果不等,那么至少有一个字符在s中。
在if的大括号中,将begin返回的迭代器赋值给it,他表示string中的第一字符。将it解引用然后传递给toupper函数。为了将toupper返回值赋值给it所指向的字符,在等号的左边再次解引用it。在我们的例子中,输出如下:
Some string
将迭代器从一个元素移动到另一个元素
为了从一个迭代器移动到下一个,可以使用自增运算符。自增迭代器在逻辑上跟自增一个整数类似。在整数中,会让整数值加一。在迭代器中,会让迭代器向前移动一个位置。
注意: 因为end返回的迭代器没有元素,所以他不能再增加,也不能被解引用。
使用自增运算符,可以重写程序,这个程序将string的第一个单词变成大写。
for(auto it = s.begin();it!=s.end() && !isspace(*it);++it)
*it = toupper(*it);
这个循环跟,3.2.3小节中的有点类似,遍历s中的字符,直到遇到了空白字符。但是循环中存取元素不是通过下标,而是通过迭代器。
通过初始化it为s.begin开始循环,条件表达式检查是否it到达了s的末尾。如果没有达到末尾,那么将解引用it的值传递给issapce,判断是否遇到了空白。在循环的最后,++it让it指向下一个位置。
循环体,跟前面程序中的if大括号里面的类似。将it解引用传递给toupper,然后再将结果赋值给*it。
关键概念:泛型编程
从c或者java转到c++的程序员,可能有点惊讶,在for的循环中使用了!=,而没有使用<.例如上面的程序,或者94页(原书)中的程序。c++程序员习惯使用!=,跟他们使用迭代器而不是下标的原因相同:这种代码风格,被标准库提供的所有容器支持。
正如所见,只有少部分的库类型,vector,string支持下标操作。而所有的库类型都支持迭代器操作,他们实现了==和!=运算符。而这些迭代器没有实现<运算符。因此,使用迭代器和!=,不必担心正在处理的容器的具体的类型。
迭代器类型
正如我们不知道vector和string的size_type的具体的类型一样。我们也不知道,通常也不必要知道,一个迭代器的精确类型。跟size_type一样,标准库给迭代器定义了类型,叫做iterator或者const_iterator。
vector::iterator it; //it可以读写vector中的元素
string::iterator it2; //it2可以读写string中的字符
vector::const_iterator it3; //it3可以读,但是不能写元素
string::const_iterator it4; //it4可以读,但是不能写字符
一个const_iterator 的行为像一个const的指针,它只能读不能写这个迭代器代表的元素。而iterator则可以读写这个元素。如果一个vector或者string是const的,那么我们就只能使用const_iterator类型。一个非const的vector或者string,则两种类型都可以使用。
术语:迭代器和迭代器类型
术语迭代器有三种不同的含义:一种表示迭代器这种概念。一种表示被容器定义的迭代器类型。一种表示迭代器对象本身。
理解迭代器是一种概念上的类型集合是非常重要的。如果一个类型支持某些操作,就可以认为这种类型是一种迭代器类型。这些操作包括,存取容器中的元素,移动元素等。
每一个容器类都定义了一个iterator的类型。这个iterator类型支持上面讲的迭代器操作。因此这种类型是一种迭代器类型。
begin和end操作
begin和end返回的类型,依赖于对象是否为const。如果对象为const那么返回的是const_iterator类型,如果对象为非const,返回类型为iterator。
vector v;
const vector cv;
auto it1 = v.begin(); //it1类型为vector::iterator
auto it2 = cv.begin(); //it2类型为vector::const_iterator
这些默认的行为,可能并不是我们想要的。当只需要读而不需要写的时候,我们更希望使用const类型,具体的原因在6.2.3小节中介绍。为了能够使用const类型,c++11新标准引入了两个新的函数:cbegin,cdn.
auto it3 = v.cbegin(); //it3的类型为vector::const_iterator
跟begin和end一样,cbegin,cend也分别返回容器的第一个元素,和尾后位置。但是他们返回的永远是const_iterator类型。
复合解引用和成员访问
当我们解引用一个迭代器时,得到这个迭代器代表的对象。如果这个迭代器对象是一个类类型,而我们又想访问这个对象的成员。例如,我们有一个元素是string的vector。我们想判断给定的元素是否为空。假如it是这个vector的迭代器,可以使用下面的代码来判断string是否为空:
(*it).empty();
(*it).empty()中的小括号是必须要有的,具体的原因将在4.1.2小节中介绍。
小括号表示将解引用作用在it上,然后再将点号运算符作用在解引用的结果上。如果没有小括号,这个点号运算符会直接作用在it上,而不是作用it解引用之后的对象上。
(*it).empyt(); //解引用it,然后调用解引用之后对象的成员函数empty
*it.empty(); //错误:尝试获取it的成员函数empty,但是it是迭代器,没有
//empty成员函数
第二条语句,将被解释为:获取it的mepty函数,但是it是一个迭代器,没有empty函数。因此,这条语句是错误的。
为了简化这种操作,c++定义了箭头运算符(->).这个运算符将解引用和成员存取运算符合成一个运算符。即,it->mem 等价于(*it).mem
例如,假如我们有一个名字叫做text的vector
for(auto it = text.cbegin();it!=text.cend()&&!it->empty();++it)
cout << *it << endl;
初始化it指向第一个元素。只要遇到了一个空的字符串,就结束循环。值得注意的是,这个程序中只有读没有写,因此使用cbegin和cend来控制遍历。
某些对vector的操作,会使迭代器失效
在3.3.2小节中,我们注意到vector可以动态的增长,但是也有一些副作用。其中一个副作用是:不能在范围for语句的循环体内向vector增加元素。另外一个副作用是:任何改变vector大小的操作,例如push_back.都会使vector的迭代器失效。在9.3.6小节中将会介绍迭代器失效的细节。
迭代器的自增,让迭代器每次移动一个元素。所有的库容器都有迭代器,这些迭代器都支持自增操作。相似的,可以使用==和!=来比较两个合法迭代器。
对于string和vector的迭代器,还支持一次移动多个元素的操作。他们也支持所有的关系运算符。这些运算符经常被称作 迭代器运算。见表3.7中的描述
在迭代器上面的算数运算
迭代器可以加上(或者减去)一个整数值。表示迭代器向前(或者向后)移动某些元素的位置。当加上或者减去一个整数值之后,新的位置也必须是合法的,即,新的位置也必须指向一个合法的元素或者尾后元素。例如,我们可以计算一个vector的中间迭代器:
auto mid = vi.begin()+vi.size()/2;
如果vi有20个元素,vi.size()/2的值就是10.那么mid就等于vi.beginn()+10.谨记下标从0开始,因此这个元素等价于vi[10],表示从第一开始,相隔十个的那个元素。
除了对迭代器使用等于与不等于的比较以外,还可以对迭代器使用关系运算符。参与比较的迭代器必须是同一个容器的有效的迭代器。例如,如果it是跟mid同一vector的迭代器。可以使用下面的代码来表示,it是在mid之前还是之后:
if(it
当两个迭代器指向同一个容器时,也可以将两者相减。结果就是两个迭代器之间的距离。所谓的距离表示的是:一个迭代器需要增加多少才能跟另外一个迭代器表示的元素相等。
相减的结果的类型是一个有符号的整数类型,叫做:difference_type.
vector和string都定义了difference_type类型。这个类型是有符号的,因为相减可能产生负数。
使用迭代器运算
使用迭代器运算的一个经典算法是,使用二分法进行搜索。二分法搜索是在一个排序队列中搜索某个值。从中间位置开始搜索,如果找到了,就停止搜索。如果没有找到,并且中间元素小于搜索的元素,那么,就在序列的后半部分搜索。如果大于搜索的元素,就在前半部分寻找,直到找到,或者搜寻完所有的元素。
例子如下:
//文本必须被存储
//beg和end 用来表示搜索的范围
auto beg = text.begin(),end=text.end();
auto mid = text.begin() +(end-beg)/2; //原序列的中点位置
while (mid != end && *mid !=sought){
if(sought < *mid) //是否寻找的元素在前半部分
end = mid; //如果是调整范围忽略后半部分
else
beg = mid +1; //否则在后半部分
mid = beg+(end-beg)/2; //新的中间的点
}
通过定义三个迭代器开始程序:beg是范围的第一个元素。end是范围的最后一个元素的后面的位置。mid是最接近中间的那个元素。初始化这三个迭代器来表示名字为text的vector的范围。
循环首先判断是否为空。如果mid等于end的值,表示已经遍历完。这种情况下,条件表达式返回false,退出while循环。否则,mid就指向了某个元素,然后再次判断这个元素是否等于我们需要寻找的那个,如果是,则表示已经找到,然后退出while循环。
如果仍然有元素需要处理,那么在while里面的代码调整end和beg的值来调整搜寻的范围。如果mid表示的元素的值大于sought,那么sought就会出现在前半段。因此,忽略mid之后的后半部的元素。
通过将mid的值赋值给end来忽略。如果*mid的值小于sought,那么想要寻找的元素在序列的后半段。此种情况下,将beg调整为mid后面的哪一个元素。因为我们已经知道mid不是我们需要的那个元素,所以重新从新的范围内计算出新的mid。
while循环结束,mid要么等于end,要么就表示是我们寻找的那个元素。如果等于end则表示text中没有我们寻找的元素。
数组是类似于vector的一种数据结构。但是在性能和灵活性上面又有不同的权衡。跟vector类似,数组是一组同类型的无名字的对象的容器,这些对象可以通过下标来访问。跟vector不同的是:数组的大小固定,不能够向数组中添加元素。因此对于某些程序来说,他提供了更好的性能,但是在灵活性上面就有所折扣。
提示: 如果你不知道你需要多少元素,那么就使用vector
3.5.1 定义并初始化内置类型的数组
数组是一种复合类型。数组声明符有如下的格式a[d].a是数组名,d是维度。维度指定了数组元素的个数,维度必须大于零。元素的个数是数组类型的一部分。因此,维度必须在编译时就被确定下来。这就意味着维度必须是一个常量表达式。
unsigned cnt = 42; //不是一个常量表达式
constexpr unsigned sz = 42; //常量表达式
int arr[10]; //十个int的数组
int *parr[sz]; //42个指向int的指针数组
string bad[cnt]; //错误:cnt不是一个常量表达式
string strs[getsize()]; //如果get_size是一个常量表达式,那么这个就是正确的,否则错误
默认情况下,在数组中的元素被默认初始化。
警告: 正如内置类型的变量一样,一个定义在函数内部的内置类型的数组,默认初始化也是未定义的。
当我们定义一个数组的时候,我们必须给数组指定一个类型。不能使用auto关键字,让编译器从初始值中推断类型。跟vector一样,数组保存的是一个对象。因此不会有引用的数组。
显示的初始化数组元素
我们可以使用列表初始化数组元素。当使用列表初始化元素的时候,我们可以省略数组的维度。当省略了数组的维度,编译器从初始值的个数中推导出维度的大小。如果指定了数组的维度,那么初始值的个数不能超过维度的大小。如果维度大于初始值的个数,那么初始值用来初始化靠前的元素,剩下的元素使用默认初始值。
const unsigned sz = 3;
int ial[sz] = {0,1,2}; //三个int,分别为0,1,2,
int a2[] = {0,1,2}; //维度为三的数组
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','+','+'}; //列表初始化,没有null
char a2[] = {'c',‘+’,‘+’,‘、0’};//列表初始化,显示的添加一个null
char a3[] = "c++"; //null自动被添加
const char a4[6] = "Daniel"; //错误:没有空间存放null字符。
a1的维度为3.a2和a3的维度为4.a4的定义是错误的。尽管字符字面量表面上包含6个字符,但是相应的数组的大小应该至少为7,因为有一个是用来存放空字符的。
不许拷贝和赋值
不能将一个数组拷贝到另外一个数组的方式来初始化数组。也不能将一个数组赋值给另外一个数组。
int a[] ={0,1,2}; //三个int的数组
int a2[] = a; //错误:不能用一个数组拷贝到另外一个数组的方式来初始化数组
a2 = a; //错误:不能将一个数组赋值给另外一个数组
警告:
一些编译器允许数组的赋值,这是编译器的扩展功能。使用编译器的非标准特性,并不是一个好的习惯,因为这样会导致在其他的编译器上面可能无法编译。
理解复杂数组类型的声明
跟vector一样,数组可以保存大多数类型的对象。例如,可以定义指针的数组。因为数组是对象,所以也可以定义这个对象的指针和引用。定义一个保存指针的数组,通常比较好理解。但是定义一个指针或者引用指向一个数组,就有点复杂了。
int * pts[10]; //ptrs是一个含有十个指针的数组。指针指向int
int &refs[10] = /*??*/ //错误:没有引用的数组。
int (*parray)[10] = &arr; //parray 指向十个元素的数组。
int (&arrRef)[10] = arr; //arrRef绑定到了十个元素的数组。
默认情况下,类型声明符从右到左阅读。从右到左阅读ptrs的定义是容易理解的:我们看到,定义了一个大小为10的数组,名字为ptrs,它保存的类型为指针,指针指向int类型。
从右到左读parray的定义,用处不大。因为数组维度的定义是直接跟在数组名的后面。所以从内到外的阅读比从右到左的阅读更容易些。从内到外的阅读更容易理解parray的定义。先看小括号内的定义*parray.这个意味着这是一个指针。然后看右边,parray指向一个大小为10的数组。然后再看左边,表示数组的元素为int。因此,parray是一个指针,这个指针指向含有十个int元素的数组。
类似的,(&arrRef)表明arrRef是一个引用。它绑定的是一个大小为10的数组。数组的元素为int类型。
当然,c++对类型声明符的多少没有任何限制
int *(&array)[10] =ptrs; //array是一个引用,绑定到了一个含有十个int指针的数组。
从内到外的阅读这个声明:array是一个引用,然后看右边,这个引用绑定到了一个含有十个元素的数组。再看左边,表明这个数组的元素为int类型的指针。
因此,array是一个引用,这个引用绑定到了一个含有十个int类型指针的数组。
提示: 要想理解数组的声明,应该从数组的名字开始,然后由内向外进行解读。
跟标准库类型vector和sring类型一样,我们可以使用范围for语句和下标存取数组中的元素。下标从0开始。例如,一个含有十个元素的数组,它的下标是0-9,而不是1-10.
当使用一个变量来表示数组大小的时候,通常应该将这个变量的类型定义为size_t.size_t是一种机器相关的无符号类型,它能够保证任何在内存中的对象的大小都能够满足。size_t类型定义在cstddef头文件中,这个头文件是c头文件stddef.h的c++版本。
除了数组的大小被固定以外,使用数组的方式跟使用vector的方式几乎类似。例如可以使用数组保存各个分数段的个数来重新实现3.3.3小节中的程序。
unsigned scores[11] = {};
unsigned grad;
while(cin >> grade){
if(grade <= 100)
++scores[grade/10];
}
上面这个程序与3.3.3小节中的唯一不同,就是scores的声明不同。在这个程序中scores是一个含有11个无符号元素的数组。另外一个不太显眼的区别是,这个程序中的下标是c++定义的,可以直接用在数组类型的对象上。而3.3.3小节中的下标是通过vector的模板定义,只能运用于vector类型的对象上。
跟string和vector的情况一样,当我们想要遍历数组总的所有元素的时候,最好使用范围for语句。例如,我们要打印scores中的结果,可以如下形式:
for(auto i:scores)
cout << i <<" "
cout << endl;
因为数组的维度是数组类型的一部分,所以系统知道scores有多少元素。使用范围for语句意味着我们不必自己管理遍历的一些细节。
检查下标值
跟string和vector一样,下标值需要程序员来保证在合理的范围内,即,下标值要大于等于0,并且小于数组的大小。为了防止数组越界,你需要小心谨慎并且对小标进行彻底的测试。对于能够编译并且能够执行的程序来说,含有这种错误也是有可能的。
c++中指针和数组联系紧密。事实上,我们马上就会介绍到,当我们使用一个数组的时候,编译器通常转换数组成一个指针。
通常,使用取地址运算符来获取指向某个对象的指针。取地址运算符可以用在任何对象上。数组中的元素是对象。当数组使用下标时,结果就是:得到在这个数组中的这个位置上面的对象。和其他对象一样,通过使用取地址运算符,可以获得数组元素的指针。
string nums = {"one","two","three"};
sting *p = &nums[0]; //p指向数组第一个元素
但是,数组有一个特殊的属性——在使用数组的大多数场景中,编译器自动将数组替换成一个指向首元素的指针。
string *p2 = nums; //等价于 p2 = &nums[0]
注意: 在大多数表达式中,我们使用的数组对象,其实使用的是这个数组的首元素的指针
这里有各种各样的例子可以证明,使用数组,就相当于使用数组首元素指针。第一例子就是:当我们使用一个数组作为另外一个auto类型变量的初始值时,推导出来的类型是一个指针,而不是数组:
int ia[] = {0,1,2,3,4,5,6,7,8,9};//ia 是十个元素的数组
auto ia2(ia); //ia2是int * 类型,指向ia第一个元素
ia2 = 42; //错误:ia2是一个指针,并且不能将一个int值赋值给一个指针
尽管ia是一个数组,但是当使用ia作为一个初始值的时候,就跟使用下面的格式差不多:
auto ia2(&ia[0]);
值得注意的是:当我们使用decltype的时候,这种转换不会发生。decltype(ia)返回的类型就是一个含有十个元素的int数组。
decltype(ia) ia3 = {0,1,2,3,4,5,6,7,8,9};
ia3 = p; //错误:不能将一个int* 赋值给一个数组
ia3[4] = i; //正确:将i的值赋值给ia3中的元素。
指针也是迭代器
用于定位数组中元素位置的指针除了在2.3.2节中介绍的操作以外,还有其他操作。事实上,指向一个数组元素的指针支持跟vector和strings的迭代器一样的操作。例如,可以使用自增运算让数组中的元素移动到另外一个元素。
int arr[] = {0,1,2,3,4,5,7,8,9};
int *p = arr; //p指向arr的第一个元素
++p; //p指向了arr[1]
跟我们可以使用迭代器遍历vector一样,我们也可以使用指针遍历一个数组。为了做到这种操作,我们当然得先获取到第一个元素的指针和最后一个元素后面位置的指针。正如所见,可以使用数组本身或者对第一个元素使用取地址运算符来表示第一个指针。我们还可以使用数组的其他特性来表示数组的尾后指针。通过取最后一个元素下面那个元素的地址来得到尾后指针,这个元素是一个在数组总不存在的元素。
int * e = &arr[10]; //最后一个元素后面的那个元素的指针
此处,我们使用下标运算符到一个不存在的元素上。arr元素的大小为10,因此最后一个元素的索引值应该为9。唯一能对这个元素做的操作是取地址,然后用其来初始化e。就像尾后迭代器一样,尾后指针不指向元素。因此不能解引用或者自增一个尾后指针。
使用这些指针,我们可以写一个循环来打印arr中的元素:
for(int *b = arr;b!e;++b)
cout << *b << end; //打印arr中的元素
begin和end标准库中的函数
尽管我们可以通过计算获得尾指针,但是,这样更容易出错。为了使用指针更加容易和安全。标准库引入了两个新的函数,begin和end。这两个函数的行为,就跟容器类的同名函数的行为类似。因为数组不是类类型,所以这些函数不是成员函数。取而代之的是,将数组作为参数传进去。
int ia[] = {0,1,2,3,4,5,6,7,8,9}; //ia是十个int的数组。
int *beg = begin(ia); //指向数组中的第一个元素
int *last = end(ia); //指向数组中的最后一个元素
begin 返回给定数组的第一个元素指针。end返回给定数组的最后一个元素后面的那个指针。这些函数所在的头文件为iterator。
使用begin和end特别容易通过循环来处理数组中的元素。例如,arr是一个int类型的数组,可以使用下面的代码来找出第一个负数。
int *pbeg = begin(arr); *pend = end(arr);
while(pbeg != pend && *pbeg >= 0)
++pbeg;
首先定义两个int类型的指针,pbeg和pend。pbeg指向数组中的第一个元素。pend指向最后一个元素后面那个元素。
然后使用while循环,在while的条件表达式中,使用pend让解引用pbeg能够更安全,即不会超出数组的界限。然后解引用pbeg,并接续判断是否为负数,如果是负数,那么循环结束。如果不是,pbeg自增,然后指向下一个元素。
注意: 指向最后一个元素后面位置的指针,他的行为,跟vector的尾后指针一样,不能对其进行解引用,和自增。
指针运算
指向数组元素的指针可以使用表3.6,和表3.7中的所有迭代操作。这些操作包括——解引用,自增,比较,与一个整数值相加,两个指针相减。当指向内置类型的数组元素的指针使用这些操作的时候,就跟迭代器的使用是一样。
当加上一个整数,或者减去一个整数之后,得到一个新的指针。新的指针指向原始数据前面或者后面的位置。
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[4],arr的最后一个元素。
将ip加4之后,得到一个新的指针。这个指针指向的位置为,原始位置向后移动4个位置。将一个指针相加之后,指向的位置,最好是在同一个数组中,或者是同一个数组的尾后位置。
//正确:arr指向第一个元素,p指向arr的尾后位置
int *p = arr + sz; //注意,不要解引用这个指针
int *p2 = arr + 10; //错误:arr只有5个元素,p2的值是未知的。
当我们将sz加到arr上时,得到一个新的指针,这个指针,指向sz位置后面的元素。编译器不会检查,超出正常范围的错误指针。
跟迭代器一样,将两个指针相减得到,这两个指针相距的距离,这两个相减的指针必须是同一个数组中的指针。
auto n = end(arr) - begin(arr);//n是5,arr的元素个数。
上面相减所得结果的类型为ptrdiff_t.跟size_t类似,ptrdiff_t类型是机器相关的,并且定义在了cstddef头文件中。因为相减可能会产生负数,所以ptrdiff_t是一个有符号的整数类型。
还可以使用关系运算符对数组中的元素指针进行比较,例如,可以使用下面的方式遍历数组。
int *b = arr,*e = arr + sz;
while(b < e){
++b;
}
不能对指向不同对象的指针,使用关系运算符。
int i = 0, sz = 42;
int *p = &i, *e = &sz;
// 结果未知:p和e是不相关的,这样比较没有任何意义
while(p < e)
...
解引用和指针运算之间的交互
将一个指针和整性值相加,结果也为一个指针。如果结果指针也指向一个元素,那么可以对结果指针执行解引用运算。
int ia[] = {0,2,4,6,8}; //五个元素的int类型数组。
int last = *(ia+4); //正确:初始化last为8,ia[4]的值
表达式*(ia+4)计算ia后面4个元素的位置,然后再对这个新位置的指针执行解引用。这个表达式等价于ia[4].
回忆一下,3.4.1小节中的代码,在解引用和点运算符之间最好在有必要的地方使用小括号。同样的,此处的小括号也是必须的。如果写成下面的样子:
last = *ia + 4; //正确:last =4,等价于 ia[0] + 4;
上面的代码表示:先解引用ia,然后在将解引用的值和4相加。会形成这种结果的原因将会在4.1.2小节中介绍。
下标和指针
正如所见,在使用数组名的大多数地方,我们实际使用的是这个数组中第一个元素的指针。最好的例子,那就是使用下标。给定下面的代码:
int ia[] = {0,2,4,6,8}; //含有五个int类型的数组
此时,ia[0]是一个使用了数组名的表达式,对数组执行下标运算,实际上,下标的执行的对象是一个指向了元素的指针。
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)
//p[1] 跟ia[3]是同一个元素
int k = p[-2]; //p[-2]跟ia[0]是同一个元素
最后一个例子,指出了数组和标准容器库vector,string在下标运算上有一个不同。标标准库类型强制要求下标索引值为无符号。而内置的下标操作没有这个限制。内置类型的下标操作可以为负数。当然,就算是负数下标,那么结果也必须是指向同一个数组中的元素,或者尾后元素。
警告 跟vector和string的下标操作不同,内置类型的下标操作不是一个无符号类型。
警告: 尽管c++支持c风格的字符串,但是这种风格最好不用使用。c风格的字符串常常引起bug,并且带来安全问题。
字符串字面量是一种通用的实例,这种实例是c++继承自c风格的字符串。c风格的字符串不是一种类型,而是,为了 方便表达和使用字符串的一种简化。这种简写的字符串被存储在字符数组中,并且末尾为null字符。通常我们使用指针来操作这些字符串。
c库中的字符串函数
标准c库提供了一些列的函数,如表3.8.这些函数操作c风格的字符串。这些函数被定义在了cstring头文件中,这个头文件是string.h的c++版本。
警告
表3.8中的函数,不会检查她们的字符串参数
传递到这些函数的字符串,末尾必须是空字符。
char ca[] = {‘c’,’+’,’+’}; //不是以空字符,结尾
cout << strlen(ca) << endl; //错误:ca没有以空字符结尾
在此处中,ca是一个字符数组,但是他没有以空字符结尾。因此此处的结果是未知的。这种用法最可能的影响是strlen一直保持搜索,直到遇到第一个空字符。
比较字符串
比较两个c风格的字符串跟比较两个string类型完全不同。当我们比较string的时候,直接使用关系运算符和等于运算符
string s1 = “A string example”;
string s2 = “A different string”;
if(s1< s2) //返回false,s2比s1小
使用这些运算符在c风格的字符串,比较的是指针的值,而不是字符串本身。
const char cal[] = “A string example”;
const char ca2[] = “A different string”;
if(cal < ca2) //结果未知:比较的是两个不相关的地址
记住:当我们使用数组的时候,实际上使用的是这个数组第一个元素的指针。因此,这个条件表达式比较的是两个 const char*。这些指针不是指向一个相同的对象,因此结果是未知的。
为了比较字符串,而不是指针,我们可以调用strcmp.该函数在两个字符串相等的时候,返回0,根据第一个字符串是大于还是小于,返回一个正数,或者一个负数。
if(strcmp(cal,cal2) < 0)//跟string的比较 s1
目标字符串的大小,由调用者负责
连接或者复制一个c风格的字符串跟库类型string也是不一样的。例如,如果想连接上面的string类型的s1和s2,我们可以直接写成如下的形式:
string largeStr = s1+” ”+s2;
对cal1和cal2使用相同的操作,是错误的。cal1+cal2尝试将两个指针相加,它是非法和没有任何意义的。
我们可以使用strcat和strcpy函数来达到同样的效果。我们还需要传递一个数组去保存操作之后产生的结果字符串。这个数组必须足够大,能够保存结果字符串,包括结尾的空字符。
此处展示代码,虽然常见,但是极易诱发严重错误:
strcpy(largeStr,cal); //复制cal到largeStr
strcat(largeStr,” ”); //增加一个空格字符在largeStr的末尾
strcat(largeStr,ca2); // 连接ca2到largeStr
上述容易产生的潜在问题是:我们可能误算largeStr需要的大小。并且,在改变largeStr存储内容的任何时候,都应该再次计算largeStr需要的大小。不幸的是,这种代码,到处都是,常常导致严重的安全漏洞。
提示: 对于大多数应用程序而言,为了更加安全,更应该使用库类型string而不是c风格字符串类型。
许多c++程序早于标准库,并且没有使用string和vector类型。而且有些程序是用c或者其他语言写的c++接口程序,他们不能使用c++库。因此,用现代c++编写的程序可能不得不和使用数组或者c风格字符串的程序衔接。为了使衔接更加容易,c++标准库提供了一些工具。
混合库类型string和c风格的字符串
在3.2.1中我们看到,可以通过字符串字面量来初始化string
string s(“Hello World”); //s保存有Hello World
更通常的说法是:在可以使用字符串字面量的地方,就能使用空字符结尾的字符数组。
1. 可以使用空字符结尾的字符数组,去初始化或者赋值给一个string
2. 可以使用空字符结尾的字符数组,作为加号运算符的一个操作数,还可以作为复合加号运算符(+=)的右操作数。
相反,这个功能是不支持的:当一个c风格的字符串需要的时候,没有直接的方式使用库string来代替。例如,不能通过string来初始化一个字符指针。但是,string提供了一个成员函数c_str,它可以完成这个操作:
char *str = s; //错误:不能通过string来初始化char *
const char *str = s.c_str(); //正确
c_str函数返回一个c风格的字符串。即,它返回一个指针,这个指针指向一个空字符结尾的字符数组,这个字符数组保存着跟string相同的字符。这个指针的类型为const char *.它防止我们改变这个字符数组中的值。
警告: 如果一个程序需要连续存取由c_str()函数返回的数组内容。程序必须拷贝一个由c_str函数返回的副本。
使用一个数组初始化vector
在3.5.1中,我们注意到不能够用数组来初始化一个内置类型的数组。同样也不能使用vector来初始化一个数组。但是,可以使用数组来初始化vector。为了这个初始化,需要指定被复元素的起始地址和结尾地址。
int int_arr[] = {0,1,2,3,4,5};
//ivec有六个元素,每一个都是int_arr对应位置的复制。
vector ivec(begin(int_arr),end(int_arr));
两个指针,标记了用于初始化ivec的值的范围。第二个指针,指向了要复制数据最后一个元素的后面一个元素。此处,我们使用了库函数begin和end,将int_arr的头指针和尾后指针。因此ivec有六个元素,每个元素的值跟int_arr相应位置的值相等。
指定的值可以是数组的一个子集。
//复制三个元素,int_arr[1],int_arr[2],int_arr[3]
vector subVec(int_arr+1,int_arr+4);
这个初始化使用三个元素来初始化subVec,值分别为int_arr[1]到int_arr[3].
建议: 指针和数组,容易引起错误。部分错误是因为指针属于底层操作,常常繁琐,不好理解。另外一个原因是,声明指针,引起的语法错误。
现代c++程序应该使用vector和迭代器,而不是内置类型的数组和指针,应该使用string而不是使用c风格的字符串。
严格来讲,c++中并没有多维数组。通常意义上来讲的多维数组是数组的数组。谨记这个对于多位数组的使用是非常有帮助的。
通过提供两个维度来定义一个元素也是数组的数组:数组自身的维度和他元素的维度。
int ia[3][4]; //数组大小为3,每一个元素是4个int类型的数组。
int arr[10][20][30]={0};//数组大小为10,每个元素为含有20个元素的数组,而这二十个元素中的每一个元素又是一个含有30个int类型的数组。
正如3.5.1小节中所见,从内到外的阅读这种定义更易于理解。从ia的定义开始,ia是一个3大小的数组,继续往右看,可以看到ia的元素有维度。因此ia中的元素本身也是一个大小为4的数组。然后往左阅读,这些元素的类型为int。因此,ia是一个大小为3的数组,每个一元素又是4个int类型的数组。
使用相同的方法阅读arr的定义。首先arr是一个大小为10的数组,他的元素为大小20的数组,这个数组的每一个元素为30个int类型的数组。对于使用多少个下标,c++没有限制。即,可以定义,数组的数组的数组…
在二维数组中,第一维通常被当作行,第二维被当作列。
初始化多维数组的元素
跟任何数组一样,可以通过大括号的初始值列表来初始化多维数组。下面的代码每一行都使用了大括号来初始化:
int ia[3][4] = {//三个元素,每个元素是大小为4的数组
{0,1,2,3},//初始化索引为0的行
{4,5,6,7},//初始化索引为1的行
{8,9,10,11}//初始化索引为2的行
};
内嵌的大括号是可选的,下面的代码,虽然去掉括号,但是跟上面的初始化等价:
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};
只有第一行被初始化,其他的元素都被初始化为0
多维数组的下标操作
跟数组一样,可以使用下标来存取多位数组中的元素。在多维数组中,对于每一个维度,使用一个下标。
如果一个表达式有和它维度一样多的下标,那么就可以得到指定类型的元素。如果下标数少于维度,那么得到的就是指定索引的内部数组。
//赋值arr的第一个元素,给ia的最后一个元素
ia [2][3] = arr[0][0][0];
int (&row)[4] = ia[1];//绑定ia第一行的四个元素到row上面
在第一个例子中,两个数组都提供了所有维度的索引。在左边,ia[2]返回ia的最后一行。他没有返回ia中的元素,但是返回了一个数组。当再次下标操作这个返回的数组时,获取元素[3],他是返回数组中的最后一个元素。
类似的,右边的操作数有三个维度。首先获取到最外层数组索引为0的元素,这个元素是大小为20的数组。然后再次获取这个维度为20数组的第一个元素,它返回一个大小30的另外一个数组。最后从这个大小为30的数组中获取它的第一个元素。
在第二个例子中,我们定义row为一个引用,他被定义为一个大小为4的int类型的数组的引用,然后将row绑定到ia的第二行。
在其他例子中,使用内嵌的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语句遍历ia中的每一个元素,这些元素是一个一维数组。内层for语句遍历这些一维数组。此例中,我们给每个元素赋值为它在整体数组中的索引号。
多维数组使用范围for语句
在c++11新标准的下,可以使用范围for语句,重写上面的例子:
size_t cnt = 0;
for(auto &row:ia)
for(auto &col:row){
col = cnt;
++cnt;
}
这个循环跟上一个例子中的的效果一样,但是这个循环让系统来管理索引。我们想改变元素的值,因此,声明了控制变量row和col,他们两者都是引用。第一个for语句遍历ia中的元素。这些元素是大小为4的数组。因此row绑定到了一个大小为4的int类型数组。第二个for遍历这些大小为4的数组。所以,col是int类型的引用。在每次遍历中,将cnt的值赋值给ia的元素,然后自增cnt。
在上面的例子中,因为要改变数组中的值,所以使用了引用作为控制变量。但是还有更深层次的原因要使用引用。考虑如下的例子:
for (const auto &row:Ia)
for(auto col:row)
cout << col << end;
这个循环不改变元素的大小,但是还是将外层的for语句中的控制变量声明为引用。这样做是为了避免数组到指针的转换。如果我们去掉引用,写成下面的样子:
for (auto row:ia)
for(auto col:row)
程序编译失败。因为第一个for语句遍历ia,元素是大小为4的数组,当初始化row的时候,会把每一个数组元素转换成一个指针,这个指针就是这个数组的第一个元素的地址。因此,外层循环中的row的类型为int *.到内层for循环之后,就变成了非法的语句了。这个循环尝试遍历一个int *。
注意: 为了在多维数组中使用范围for语句,除了最内层的循环以外,其他循环必须使用引用。
指针和多维数组
跟数组一样,当我们使用多维数组的名字的时候,自动将其转换成指向第一个元素的指针。
注意: 当你定义一个指针指向一个多维数组的时候,谨记一个多维数组实际上就是一个数组的数组。
因为多维数组实际上是一个数组的数组,因此指向多维数组的指针实际上指向的是这个数组的内层的第一个数组:
int ia[3][4]; //大小为3的数组,他的元素是一个大小为4的int数组
int (*p)[4] = ia; //p指向一个大小为4的int类型数组
p = &ia[2]; //p现在指向了ia的最后一个元素
根据3.5.1小节中的策略,首先从*p开始,可以知道p是一个指针,然后看右边,可以知道p指向的对象为4维度的数组。然后再看左边,这个数组的元素类型为int。因此,p是一个指针,指向一个含有4个int的数组。
注意: 此处声明中的小括号是重要的:
int *ip[4]; // int指针数组 int (*ip)[4]; // 指针,指向数组,数组大小为4,元素类型为int
随着c++11新标准的提出,通过使用auto和decltype关键字,常常可以减少写一个指向数组的指针的繁复:
for(auto p = ia; p!=ia+3;++p){
for(auto q=*p;q!=*p+4;++q)
cout << *q << ‘ ’;
cout << end;
}
外层for循环,初始化p为ia的第一个元素。循环继续处理直到处理完ia中的三个元素。p的自增,跟p移动到下一个元素,效果一样。
内层for循环打印内层数组中的元素。先初始化q为内层数组的第一个元素。*p的结果是一个大小为4的int类型数组。跟平常一样,当我们使用一个数组的时候,他自动将其转换成指向第一个元素的指针。内层循环继续处理,直到处理完所有内层数组中的所有元素。为了获取内层数组中的尾后指针,将p解引用之后加了4。
当然,可以使用库函数begin和end,这样更容易操作
for(auto p = begin(ia);p!=end(ia);++p){
for(auto q=begin(*p);q!=end(*p);++q)
cout << *q << ‘ ’
cout << end;
}
此处,我们让库来决定结尾指针的大小,并且使用auto,也避免了显示声明begin函数返回的类型。在外层循环中,这个类型是一个指针,该指针指向一个大小为4的int类型数组。在内层数组中,这个类型也是一个指针,不过指向的是一个int类型。
类型别名简化多维数组的指针
一个类型声明可以让阅读,编写和理解多维数组的指针,更加容易。例如:
using int_array = int[4]; //新风格的类型声明,见2.5.1
typedef int int_array[4];
for(int_aaray *p = ia;p!=ia+3;++p){
for(int *q=*p;q!=*p+4;++q)
cout << *q << ‘ ’
cout << end;
}
此处,先定义了一个int_array类型名,它表示“4个int的数组”。然后在外层循环中使用这个类型名来定义循环的控制变量。
小节概述
略
专业术语
略