除了第二章介绍的内置类型以外,C++语言还定义一个内容丰富的抽象数据类型库。其中,string和vector是两种最重要的标准库类型,前者支持可变长字符串,后者则表示可变长的集合。还有一种标准库类型是迭代器,它是string和vector的配套类型,常被用于访问string中的字符和vector中的元素。内置数组则是更基础的类型,string和vect都是对它的某种抽象。第二章介绍的内置类型是由C++语言直接定义的。这些类型,比如数字和字符,体现了大多数计算机硬件本身具备的能力。标准库定义了另一组具有更高级性质的类型,它们尚未直接实现到计算机硬件中。
string和vector分别表示可变长的字符序列,vector存放的是某种给定类型对象的可变长序列。本章还将介绍内置数组类型,和其他内置类型一样,数组的实现与硬件密切相关。因此相较于标准库类型string和vector,数组在灵活性上稍显不足。
在学习标准库类型之前,先来学习一种访问库中名字的简单方法。
到目前为止我们使用的库函数基本上都属于命名空间std,而我们编写的程序也都显式的将这一点标示出来了。例如,std::cin表示从标准输入读入内容。此处使用作用域操作符(::)的含有是:编译器应从操作符左侧名字所示的作用域中寻找右侧那个名字。 因此这条语句的含义就是要使用命名空间std中的名字cin。
之前我们说过,这种方法较为繁琐,有两种更为简单的途径也能使用到命名空间中的成员。第一种是最为安全的方法,也就是使用using声明(using declaration)。第二种比较有风险在本节第18.2.2节会介绍。
标准库类型string表示可变长的字符序列,使用string类型必须首先包含string头文件。作为标准库的一部分,string定义在命名空间std中。本节将介绍最常用的string操作,9.5节还会介绍另一些。
下面介绍几种初始化string对象最常用的方式:
string s1;//默认初始化,s1是一个空串
string s2=s1;//s2是s1的副本
string s2(s1);//同上
string s3("hiya");//s3是字面值hiya的副本,除了字面值最后的那个空字符外
string s3="hiya";//同上
string s4(10,'c');//把s4初始化为连续10个字符c组成的串
直接初始化和拷贝初始化
由2.2.1节的学习可知,C++语言有几种不同的初始化方式,通过学习string我们可以清楚的看到这些初始化方式有什么区别和联系。如果使用等号(=)初始化一个变量,实际上执行的是拷贝初始化(copy initialization),编译器把等号右侧的初始值拷贝到新创建的对象中去。与之相反如果不使用等号,则执行的是直接初始化(direct initialization)。
当初始值只有一个时,使用直接初始化和拷贝初始化都行。如果像上面s4那样初始化要用到的值有多个,一般来说只能使用直接初始化的方式。如果非要用拷贝初始化的方式来处理也不是不可以,不过需要显式的创建一个(临时)对象用于拷贝:
string s8=string (10,'c');//拷贝初始化
s8的初始值是string(10,‘c’),它实际上是用10和c两个参数创建出来的string对象,然后这个对象又拷贝给了s8.这条语句本质上等于下面两条语句:
string temp(10,'c');
string s8=temp;
可以看到s8这种初始化方式尽管合法,但可读性较直接初始化差,且没有任何补偿优势。
一个类除了要规定初始化其对象的方式外,还要定义对象上所能执行的操作。其中,类既能定义通过函数名调用的操作,就像isbn函数那样,也能定义<<、+等各种运算符在该类对象上的新含义。下面列举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;//用s2的副本代替s1中原来的字符
s1==s2;//如果s1和s2中所含的字符完全一样,则它们相等,返回true,对大小写敏感
s1!=s2;//如果不相等,返回false
<,<=,>,>=;//利用字符在字典中的顺序进行比较,且对字母的大小写敏感
读写string对象
第一章曾经介绍过,使用标准库中的iostream来读写int、double等内置类型。同样也可以使用IO操作符读写string对象:
int main(){
string s;
cin>>s;
cout<<s<<endl;
return 0;
}
在执行读取操作时,string对象会自动忽略开头的空白(即空格符,换行符、制表符等)并从第一个真正的字符读起,直到遇到下一个空白为止。如果输入的是“ hello world!”,则输出的将是“hello”,输出结果中没有空格。
和内置类型的输入输出操作一样,string对象的此类操作也是返回运算符左侧的运算对象作为其结果。因此,多个输入或输出可以连在一起。
cin>>s1>>s2;
cout<<s1<<s2;
假设给这段程序输入之前的内容,输出的将是”helloworld!“
读取未知数量的string对象
1.4.3节的程序可以读取未知数量的整数,下面编写一个类似的程序用于读取未知数量的string对象。
int main(){
string word;
while(cin>>word)
cout<<word<<endl;
return 0;
}
使用getline读取一整行
有时我们希望保留输入中的空白符,这时候就应该用getline函数来代替>>运算符。getline函数的参数是一个输入流和一个string对象,函数从给定的输入流中读取内容,直到遇到换行符为止(注意换行符也被读进来了),然后把所读内容存入到string对象中(注意不存换行符)。getline只要一遇到换行符就结束读取并返回结果,哪怕一开始就是换行符,这样返回的结果就是一个空string。触发getline函数返回的那个换行符实际上被丢弃掉了,得到的string对象中并不包含该换行符。
和输入运算符一样,getline也会返回它的流参数。因此我们也能用getline的结果作为条件。例如,我们可以通过改写上面的程序,让它一次输入一整行,而不是每行输入一个词了。
int main(){
string line;
while(getline(cin,line))
cout<<line<<endl;
return 0;
}
因为line中不包含换行符,所以我们手动的加上换行操作符。和往常一样,使用endl结束当前行,并刷新显示缓冲区。
string的empty和size操作
和Sales_item类的isbn成员一样,empty也是string的一个成员函数。调用该函数的方法很简单,只需要使用点操作符指明是哪个对象执行力empty函数就可以了。我们改写上一段代码的一部分,可以做到只输出非空的行:
while(getline(cin,line){
if(!line.empty())
cout<<line<<endl;
}
size函数返回string对象的长度(即string对象中字符的个数),我们可以通过修改上面代码的一部分做到只输出长度超过80字符的行:
string line;
while(getline(cin,line)){
if(line.size()>=80)
cout<<line<<endl;
}
string::size_type类型
对于size函数来说,返回一个int或者2.2.1节所述那样返回一个unsigned似乎是合情合理的,但其实size函数返回的是一个string::size_type类型的值。
string类及其他大多数标准库类型都定义了几种配套的类型。这些配套的类型体现了标准库与机器无关的特性,类型size_type即是其中的一种。在具体使用的时候,通过作用域操作符来表明名字size_type是在类string中定义的。尽管我们不太清楚这种类型的细节,但有一点是肯定的:它是一个无符号类型的值,而且能够放下任何string对象的大小。所有用于存放string类的size函数返回值的变量,都应该是string::size_type类型的。
因为在C++11新标准中,允许编译器通过auto和decltype来推断变量的类型:
auto len=line.size();//len的类型是string::size_type
由于size函数返回的是一个无符号整数,因此切记,如果在表达式中混用了带符号数和无符号数将可能产生意想不到的结果。例如,假设n是一个具有负值的int,则表达式s.size() 比较string对象 string对象相等意味着它们的长度相同而且包含的字符也完全相同。所有运算符都依照字典顺序: 为string对象赋值 两个 string对象相加 两个string对象相加就是把左侧运算对象与右侧运算对象串接。另外复合赋值运算符(+=)负责把右侧string对象的内容追加到左侧string对象的后面: 字面值和string对象相加 如2.1.2节所讲,即使一种类型并非所需,我们也可以使用它,不过前提是该种类型可以自动转换成所需类型。因此标准库允许把字符字面值和字符串字面值转换成string对象,所以在需要string对象的地方就可以使用这两种字面值来代替。利用这一点我们可以将上一段程序改写如下形式: 需要注意的是,当我们把string对象和字符字面值及字符串字面值混用时,必须保证每个加法运算符两侧的运算对象至少有一个是string对象: 因为某些历史原因,也为了与C兼容,所以C++语言中的字符串字面值并不是标准库类型的string对象。切记字符串字面值与string是不同的类型。 我们经常需要段都处理string对象中的字符,比如检查一个string对象是否包含空白,或者把string对象中的某个字母改为小写,再或者查看某个特定字符是否出现等。处理这类问题的关键在于如何获取字符本身。以往的经验告诉我们,处理这些情况常常涉及语言和库的很多方面。另一个关键问题是要知道能改变某个字符的特性。在cctype头文件中定义了一组标准库函数处理这部分工作。本数第82页表3.3列出了主要的函数名及其函数。 处理每个字符?使用基于范围的for语句 其中expression部分是一个对象,用于表示一个序列。declaration部分负责定义一个变量,该变量将被用于访问序列中的基础元素。每次迭代,declaration部分的变量会被初始化为expression部分的下一个元素。 for循环把变量c和str联系了起来,其中我们定义循环控制变量的方式与定义任意一个普通变量是一样的。此例中,我们通过auto关键字让编译器来决定变量c的类型,这里c的类型是char。**每次迭代,str的下一个字符被拷贝给c,因此该循环可以读作“对于字符串str的每个字符c,执行某某操作”。**此例中的操作是输出一个字符然后换行。 这里我们使用decltype关键字声明计数变量punct_cnt,它的类型是s.size函数返回值的类型也就是我们之前所说的string::size_type。 使用范围for语句改变字符串中的字符 如果想改变string对象中字符的值,必须把循环变量定义成引用类型。之前我们说过,引用只是给定对象的一个别名,因此当使用引用作为循环控制变量时,这个变量实际上被绑定到了序列的每个元素上。使用这个引用我们就能改变它绑定的字符。 每次迭代时,变量c引用string对象s的下一个字符,赋值给c也就是在改变s中对应字符的值。 只处理一部分字符 在我们编写类似的程序时,都要确认在即将访问的位置上确实有值。如果s为空,则s[0]的结果将是未定义的。 使用下标迭代 在上述程序中,for循环使用变量index作为s的下标,index的类型是由decltype关键字决定的。首先把index初始化为0,这样第一次迭代就会从s的首字符开始;之后每次迭代将index加1得到s的下一个字符。循环体负责将当前字符改写为大写形式。 使用下标执行随机访问 我们也可以直接计算得到某个下标值,然后直接获取对应位置的字符,并不是每次都得像之前一样从前往后依次访问。例如我们像要编写一个程序把0到15之间的十进制数转换成对应的十六进制形式,只需初始化一个字符串令其存放16个十六进制“数字”: 首先初始化string变量用于存放0-F的十六进制数字,注意我们将它声明成了常量,这是因为我们不希望它被改变。在循环内部使用输入值n作为hexdigits的下标,hexdigits[n]的值就是hexdigits内位置n处的字符。 标准库类型vector表示对象的集合,其中所有对象的类型都相同。集合中的每个对象都有与之对应的索引,索引用于访问对象。因为vector“容纳着”其他对象,所以它也常被成为容器(container)。第Ⅱ部分将对容器作更详细的介绍。 C++既有类模板(class template),也有函数模板,其中vector是一个类模板。只有对C++有了相当深入的理解才能写出模板,事实上,我们直到第16章才会学习如何自定义模板。幸运的是,即使我们还不会创建模板,也可以先试着使用它。 vector是模板而非类型,由vector生成的类型必须包含vector中元素的类型,例如 和任何一种类类型一样,vector模板控制着定义和初始化向量的方法。下面列出定义vecotr对象的常用方法。 第一行默认初始化vector对象,从而创建一个指定类型的空vector,看起来空vector没什么用,但我们很快就会知道程序在运行时可以很高效地往vector对象中添加元素。事实上,最常见的方式就是定义一个空vector,然后当运行时获取到元素值后再逐一添加。 列表初始化vector对象 创建指定数量的元素 值初始化 列表初始化还是元素数量? 由上边的代码可以知道,如果使用的是圆括号,可以说提供的值用来构造(construct)vector对象的。如果用的是花括号,可以表述成我们想列表初始化(list initialize)该vector对象。也就是说,初始化过程会尽可能地把花括号内的值当成是元素初始值列表来处理,只有在无法执行列表初始化时,才会考虑其他初始化方式。 尽管除了第二行以外都用了花括号,但只有v5是列表初始化。要想列表初始化vector对象,花括号里的值必须与元素类型相同。确认无法执行列表初始化后,编译器会尝试用默认值初始化vector对象。 对vector对象来说,直接初始化适用于三种情况:1.初始值已知且数量较少。2.初始值是另一个vector对象的副本。3.所有元素的初始值一样。然后更常见的情况是:创建一个vector对象时并不清楚实际所需的元素的个数,元素的值也经常无法确定。还有的时候即使元素的初值已知,但如果这些值的总量较大且各不相同,那么在创建vector对象时执行初始化操作也会显得过于繁琐。 有一点我们要弄清楚,C++标准要求vector应该能在运行时高效快速地添加元素,那么在定义vector对象时设定其大小就没什么必要了。事实上,如果设定大小可能性能会更差。只有一种例外情况:就是所有(all)元素的值都一样。一旦元素的值有所不同,更有效的办法是先定义一个空的vector对象,再在运行时向其中添加具体值。此外,在9.4节将介绍,vector还提供了方法,允许我们进一步提升动态添加元素的性能。上述的做法与C语言以及其他大多数语言中内置数组类型的用法不同。特别是用惯了C或Java,可以预计在创建vector对象时顺便指定其容量是最好的。然而事实上,通常的情况恰恰相反。 向vector对象添加元素蕴含的编程假定 除了支持push_back之外,vector还支持几种其他操作,大多数和string类似,本书表3.5列出了其中比较重要的一些: 访问vector对象中的元素的方法和访问string对象中字符的方法差不多,都是通过元素在vector对象中的位置。例如可以使用范围for语句处理vector对象的所有元素: 要使用size_type,需首先指定它是由哪种类型定义的。vector对象的类型总是包含着元素的类型: 关系运算符依照字典序进行比较:如果两个vector对象的容量不同,但是在相同位置上的元素值都一样,则元素较少的vector对象小于较多的vector;若元素的值有区别,则vector对象的大小关系由第一对相异的元素值的大小关系决定。 则输出的结果应该是: 具体实现时我们使用一个11个元素的vector对象,每个对象分别用于统计各个分数段上的分数。对于某个成绩来说,将其除以10就能得到对应的分数段索引。一旦计算得到了分数段索引,我们就能用它作为vector对象的下标,进而获取该分段的计数值并加1: 执行累加的那条语句很好地体现了C++程序代码的简洁性,表达式: 上述语句的含义是:用grade除以10来计算成绩所在的分数段,然后将所得结果作为变量scores的下标,通过运行下标运算符获取该分数段对应的计数值,因为新出现了一个属于该分数段的成绩,所以将计数值加1。 不能用下标形式添加元素 ivec本身就是空vector,不包含任何元素,所以不能用下标区访问任何元素!如前所述,正确的方法是使用push_back: 总结一下:vector对象和string对象的下标运算符可用于访问已经存在的元素,而不能用于添加元素。 如果用下标的形式去访问一个不存在的元素将引发错误,不过这种错误不会被编译器发现,而是运行时产生一个不可预知的值。不幸的是,这种通过下标访问不存在的元素的行为非常常见,而且会产生很严重的后果。所谓的缓冲区溢出(buffer overflow)指的就是这类错误,这也是导致PC及其他设备上应用程序出现安全问题的一个重要原因。确保下标合法的一种有效手段就是尽可能使用范围for语句。 我们已经知道可以使用下标运算符来访问string对象的字符或vector对象的元素,还有一种更通用的机制也可以实现同样的目的,这就是迭代器(iterator)。在第Ⅱ部分中将要介绍,除了vector之外,标准库还定义了其他几种容器。所有标准库容器都可以使用迭代器,但其中只有少数几种才同时支持下标运算符。严格来说,string对象不属于容器类型,但是string支持很多与容器类型相似的操作。vector支持下标运算符,这点和string一样;string支持迭代器,这也和vector是一样的。 和指针不一样的是,获取迭代器不是使用取地址符,有迭代器的类型同时拥有返回迭代器的成员。比如,这些类型都拥有名为begin和end的成员,其中begin成员负责返回指向第一个元素(或第一个字符)的迭代器。如有下述语句: end成员负责返回指向容器(或string对象)尾元素的下一个位置(one past the end)的迭代器,也就是说,该迭代器指示的是容器一个本不存在的尾后(off the end)元素。这样的迭代器没什么实际含义,仅是一个标记而已,表示我们已经处理完了容器中所有的元素。end成员返回的迭代器常被称作尾后迭代器(off-the-end iterator)或者简称尾迭代器(end iterator)。特殊情况下,如果容器为空,则begin和end返回的是同一个迭代器,都是尾迭代器。 迭代器运算符 和指针类似,也能通过解引用迭代器来获取它所指示的元素,执行解引用的迭代器必须合法并确实指示着某个元素(2.3.2节)。试图解引用一个非法迭代器或尾后迭代器都是未被定义的行为。 和之前的程序一样,都要先检查s是否为空。我们在if内部,声明了一个迭代器变量it并把begin返回的结果赋给它,这样就得到了指示s中第一个字符的迭代器,接下来通过解引用符将第一个字符更改为大写形式。 将迭代器从一个元素移动到另外一个元素 迭代器使用递增运算符(++)来从一个元素移动到下一个元素。从逻辑上来说,迭代器的递增和整数的递增类似,整数的递增是在 整数值上加一,迭代器递增则是迭代器向前移动一个位置。我们知道end返回的迭代器是尾后迭代器,并不实际指向某个元素,所以不能对其进行递增或解引用操作。 和我们之前编写的程序类似,上面的循环也是遍历s的字符直到遇到空白字符为止,只不过之前的程序用的是下标运算符,现在用的迭代器。循环首先用s.begin()的返回值来初始化it,这就意味着it指示的是s中的第一个字符(如果有的话)。条件部分先检查是否到达s的尾部,如果尚未到达,则将it的解引用结果传入isspace函数检查是否遇到了空白。每次迭代的最后,执行++it令迭代器前移一个位置以访问s的下一个字符。 迭代器类型 就像不知道string和vector的size_type成员到底是什么类型一样,一般我们也不知道也不必知道迭代器的精准类型。而实际上,而那些拥有迭代器的标准库类型使用iterator和const_iterator来表示迭代器的类型: const_iterator和常量指针差不多,能读取但不能修改所指元素值。相反,iterator的对象可读可写。如果vector对象或者string对象是常量,就只能使用const_iterator;如果不是常量,则既能使用iterator也能使用const_iterator。 术语:迭代器和迭代器类型 迭代器这个名词有三种含义:可能是迭代器概念本身,也可能是容器定义的迭代器类型,还可能是指某个迭代器对象。 begin和end运算符 但有时这种默认行为并非我们所要。在之后的6.2.3节中将会看到,如果对象只需读操作而无需写操作的话最好使用常量类型(比如const_iterator)。为了便于专门得到const_iterator类型的返回值,C++11新标准引入了两个新函数,分别是cbegin和cend: 这两个函数和begin和end非常类似,有所不同的是不论vector对象或者string对象本身是否是常量,返回值都是const_iterator。 结合解引用和成员访问操作 上述代码的第一圆括号必不可少,具体原因在4.1.2节介绍。该表达式的含义是先对it解引用,然后解引用的结果再执行点运算符。如果不加圆括号的话,点运算符将由it来执行,而非it解引用的结果。 我们首先初始化it令其指向text的第一个元素,循环重复执行直至处理完text的所有元素或者发现某个元素为空。每次迭代时只要发现还有元素并且尚未遇到空元素,就输出当前正在处理的元素。值得注意的是,因为循环从头到尾只是读取text的元素而并未向其中写值,所以我们使用cbegin和cend而不是begin和end来控制整个迭代过程。 某些对vector对象的操作会使迭代器失效 迭代器递增运算令迭代器每次移动一个元素,所有的标准库函数都有支持递增运算的迭代器。类似的,也能用==和!=对任意标准库类型的两个有效迭代器进行比较。 迭代器算术运算 对于string或vector的迭代器来说,除了判断是否相等,还能使用关系运算符对其进行比较。参与比较的两个迭代器必须合法而且指向的是同一个容器的元素(或者尾元素的下一位置)。例如,假设it和mid是同一个vector对象的两个迭代器,可以用下面的代码来比较它们孰先孰后: 只要两个迭代器指向的是同一个容器中的元素或者尾元素的下一个位置,就能将其相减,所得结果是两个迭代器的距离。所谓距离指的是右侧的迭代器向前移动多少位置就能追上左侧的迭代器,其类型是名为difference_type的带符号整型数 。string对象和vector对象都定义了difference_type,因为这个距离可正可负,所有它是带符号类型的。 使用迭代器运算 循环部分先检查搜索范围是否为空,如果mid和end的当前值相等,说明已经找遍了所有元素。 此时条件不满足,循环终止。当搜索范围不为空时,可知mid指向了某个元素,检查该元素是否就是我们所搜索的,如果是,也终止循环。 数组是一种类似于标准库类型vector的数据结构,但是在性能和灵活性上的权衡又和vector不同。与vector对象相似的地方是,数组也是一个存放相同类型对象的容器,这些对象本身没有名字,需要通过其所在位置访问。与vector对象不同的是,数组大小不变,不能随意向数组中增加元素。因为数组大小固定,因此对某些特殊的应用来说程序运行时性能更好,但是相应地也会损失一些灵活性。如果不清楚元素的确切个数,请使用vector。 数组是一种复合结构。数组的声明形如a[d],其中a是数组的名字,d是数组的维度,维度说明了数组中元素的个数,因此必须大于0。数组中元素的个数也属于数组类型的一部分,编译的时候维度应该是已知的。也就是说,维度必须是一个常量表达式。 默认情况下,数组的元素被默认初始化。和内置类型的变量一样,如果在函数内部定义了某种内置类型的数组,那么默认初始化会令数组含有未定义的值。 显式初始化数组元素 字符数组的特殊性 不允许拷贝和赋值 理解复杂的数组声明 默认情况下,类型修饰符从右向左依次绑定。对应ptrs来说,从右向左理解:我们定义的是一个大小为10的数组,它的名字是ptrs,然后知道数组中存放的指向int的指针。 按照由内向外的顺序阅读上述代码,首先知道array是一个引用,然后观察右边知道,array引用的对象是一个大小为10的数组,最后观察左边知道,数组的元素类型是指向int的指针。这样,array就是一个含有10个int型指针的数组的引用。要理解数组声明的含义,最好的办法就是从数组的名字开始按照由内向外(先右后左)的顺序阅读。 与标准类型vector和string一样,数组的元素也能使用范围for语句或下标运算符来访问。数组的索引同样从0开始。在使用数组下标的时候,通常将其定义为size_t类型。size_t类型是一种机器相关的无符号类型,它被设计得足够大以便能表示内存中任意对象的大小。在cstddef头文件中定义了size_t类型,这个文件是C标准库stddef.h头文件的C++语言版本。 与之前的程序相比,最大的不同是scores的声明。这里scores是一个含有11个无符号元素的数组。另外一处不同是本例所用的下标运算符是由C++语言直接定义的,这个运算符能用在数组类型的对象上。之前那个下标运算符是由库模板vector定义的,只能用在vector类型的运算对象。 因为维度是数组类型的一部分,所以系统知道数组中有多少元素,使用范围for语句可以减轻认为控制遍历过程的负担。 在C++语言中,指针和数组有着非常紧密的联系,就如我们之前所说,使用数组时编译器一般会把它转换成指针。 然而数组还有一个特性:在很多用到数组名字的地方,编译器会自动地将其转换为指向数组首元素地指针: 所以在大多数表达式中,使用数组类型的对象其实就是使用一个指向该数组首元素的指针。 由上可知,在一些情况下数组的操作实际上是指针的操作,这一结论有很多隐藏的意思。其中一层意思是当使用数组作为一个auto变量的初始值时,推断得到的类型是指针而非数组: 尽管ia是一个int型数组,但当使用ia作为初始值时,编译器实际执行的初始化过程 类似于下面的形式: 必须指出的是,如果我们使用decltype关键字时,上述转换部分发生,decltype(ia)返回的类型是由10个整数构成的数组: 指针也是迭代器 就像使用迭代器遍历vector对象的元素一样,使用指针也能遍历数组中的元素。这么做的前提是先获取到指向数组的第一个元素的指针和指向数组尾元素的下一位置的指针。不过获取尾后指针就要用到数组的另外一个特殊的性质。我们可以设法获取数组尾元素之后的那个并不存在的元素的地址: 这里使用下标运算符索引了一个不存在的元素。就像尾后迭代器一样,尾后指针也不指向具体的元素。因此不能对尾后指针执行解引用或递增操作。 利用上面得到的首元素指针和尾后指针重写之前的循环,令其输出arr的全部元素: 标准库函数begin和end 使用begin和end很容易的可以写出一个循环并处理数组中的元素。例如arr是一个整型数组,下面的程序负责找到arr中的第一个负数: while语句的条件部分通过比较pbeg和pend来确保可以安全地对pbeg解引用,如果pbeg确实指向了一个元素,将其解引用并检查元素值是否为负值。一个指针如果指向了某种内置类型数组地尾元素的下一个位置,则其具备与vector的end函数返回的与迭代器类似的功能。特别需要注意的是,尾后指针不能执行解引用和递增操作。 指针运算 给指针加上一个整数,得到的新指针仍需指向同一数组的其他元素,或者指向同一数组的尾元素的下一个位置: 当我们给arr加上sz时,编译器自动地将arr转换成指向数组arr中首元素的指针。如果计算所得的指针超出了数组的范围就将产生错误,而且这种错误编译器一般发现不了。 两个指针相减的结果的类型是一种名为ptrdiff_t的标准库类型,和siez_t一样,ptrdiff_t也是一种定义在cstddef头文件中的标准库类型。因为差值可能为负值,所以它是一种带符号类型。 如果两个指针分别指向两个不相关的对象,则不能比较它们: 尽管作用可能不是特别明显,但必须说明的是,上述指针运算(加减整数,相减,比较)同样适用于空指针和所指对象并非数组的指针。在后一种情况下,两个指针必须指向同一对象或该对象的下一位置。如果p是空指针,允许给p加上或减去一个值为0的整型常量表达式。两个空指针也允许彼此相减,结果显然为0。 下标和指针 只要指针指向的是数组中的元素(或者数组中尾元素的下一个位置),都可以执行下标运算: 虽然标准库类型string和vector也能执行下标运算,但是数组与它们相比还是有所不同。标准库类型限定使用的下标必须是无符号类型,而内置的下标运算无此要求,上面的最后一个例子很好的说明了这一点。内置类型的下标运算符可以处理负值,当然结果地址必须指向原来指针所指同一数组中的元素(或同一数组尾元素的下一位置)。 尽管C++支持C风格字符串,但在C++程序中最好不要使用它们。这是因为C风格字符串不仅使用起来不方便,而且极易引发程序漏洞,是诸多安全问题的根本原因。 这些函数不负责验证其字符串参数。 上例中,ca虽然是一个字符数组,但它不是以空字符作为结束的,因此上述程序将产生未定义的结果。strlen函数将有可能沿着ca在内存中的位置不断向前寻找,直到遇到空字符才停下。 比较字符串 如果把这些运算符用在两个C风格字符串上,实际比较的将是指针而非字符串本身: 我们要谨记之前介绍过的,当使用数组时,其实真正用的是指向数组首元素的指针。 因此上面的if条件实际上比较的是两个const char*的值。这两个指针指向的并非同一个对象,所以得到未定义的结果。要想比较两个C风格字符串需要调用strcmp函数,此时比较的就不再是指针了。如果两个字符串相等,strcmp返回0;如果前面的字符串比较大,返回正值;如果后面的字符串较大,返回负值: 目标字符串的大小由调用者指定 同样的操作如果放到ca1和ca2这两个数组身上就会产生错误,表达式ca1+ca2试图将两个指针相加,显然这样的操作没什么意义,也肯定是非法的。 一个潜在的问题是,我们在估算largestr所需的空间时不容易估准,而且largestr所存的内容一旦改变,就必须重新检查其空间是否足够。不幸的是,这样的代码到处都是,程序员根本无法照顾周全。这类代码充满了风险而且经常导致严重的安全泄漏。对大多数应用来说,使用标准库string要比使用C风格字符要更安全更高效。 很多C++程序在标准库出现之前就已经出现,它们自然没有用到string和vector类型。而且有一些C++程序实际上是C语言或其他语言的接口程序,当然也无法使用C++标准库。因此现代的C++程序不得不与那些充满了数组或C风格字符串的代码衔接,为了使这一工作简单易行,C++专门提供了一组功能。 混用string对象和C风格字符串 更一般的情况是,任何出现字符串字面值的地方都可以用以空字符的字符数组来替代: 顾名思义,c_str函数的返回值是一个C风格的字符串。也就是说,函数的返回结果是一个指针,该指针指向一个以空字符结束的字符数组,而这个数组所存的数据恰好与那个string对象的一样。结果指针的类型是const char,从而确保我们不会改变字符数组的内容*。我们无法保证c_str函数返回的数组一直有效,事实上,如果后续的操作改变了s的值就可能让之前返回的数组失去效用。如果执行完c_str函数后程序想一直都能使用其返回的数组,最好将该数组重新拷贝一份。 使用数组初始化vector对象 在上述代码中,用于创建ivec的两个指针实际上指明了用来初始化的值在数组int_arr中的位置,其中第二个指针应指向待拷贝区域尾元素的下一位置。此例中,使用标准库函数begin和end来分别计算int_arr的首指针和尾后指针。在最终的结果中,ivec将包含6个元素,它们的次序和值都与数组int_arr完全一致。 建议:尽量使用标准库类型而非数组 使用指针和数组很容易出错。一部分原因是概念上的问题:指针常用于底层操作,因此容易引发一些与繁琐细节有关的错误。其他问题则源于语法错误,特别是声明指针时的语法错误。 严格来说,C++中并没有多维数组,通常我们所说的多维数组其实是数组的数组。谨记这一点,对今后理解和使用数组大有益处。 我们理解这些高维数组应该像3.5.1节所介绍的,按照由内而外,从右向左的顺序阅读。对于二维数组来说,常把第一个维度称为行,第二个维度称作列。 多维数组的初始化 允许使用花括号括起来的一组值初始化多维数组,这点和普通的数组一样。下面的初始化形式中,多维数组每一行分别用花括号扩了起来: 其中内层嵌套着的花括号并非必需的,例如下面的初始化语句,形式上更为简洁,完成的功能和上面这段代码一样 : 类似于一维数组,在初始化多维数组时也并非所有元素的值都必须包含在初始列表之内。如果仅仅像初始化每一行的第一个元素,通过如下语句即可: 在这种情况下如果再省略掉内层的花括号,结果就大不一样了: 多维数组的下标引用 在第二个例子中,把row定义成一个含有4个整数的数组引用,然后将其绑定到ia的第二行。 外层的for循环遍历ia的所有元素,这里指的元素是指一维数组;内层的for循环则遍历那些一维数组中的整数元素。 使用范围for语句处理多维数组 这个循环赋给ia元素的值和之前那个循环是完全相同的,区别之处是通过使用范围for语句把管理数组索引的认为交给了系统来完成。因为要改变元素的值,所以得把控制变量的row和col声明成引用类型。因为第一个for循环遍历ia的所有元素,这些元素是大小为4的数组,因此row的类型就应该是含有4个整数的数组的引用。第二个for循环遍历那些4元素数组中的一个,所以col的类型是整数的引用。 这个循环中并没有任何写操作,但我们还是将外层循环控制的变量声明成了引用类型,这是为了避免数组被自动转换成指针(3.5.3节)。假设不用引用类型,则循环如下述形式: 程序将无法通过编译。在执行第一个for循环遍历ia所有元素时,这些元素实际上是大小为4的数组。因为row不是引用类型,所以编译器初始化row时会自动将这些数组形式的元素(和其他类型数组一样)转换成指向该数组内首元素的指针。这样得到的row的类型就是int*,显然内层的循环就不合法了,编译器将试图在一个int*内遍历,这显然和程序的初衷相去甚远。所以我们应当注意:使用范围for语句处理多维数组时,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型。 指针和多维数组 和普通数组一样,当程序使用多维数组的名字时,也会自动将其转换成指向数组首元素的指针。 根据我们之前的从内到外的策略,我们首先明确(*p)意味着p是一个指针。接着观察右边发现,p所指的是一个维度为4的数组;再观察左边指的,数组中的元素是整数。因此,p就是指向含有4个整数的数组的指针。我们需要注意一点:第二行的圆括号不可缺少,如果少了圆括号,即: 外层的for循环首先声明一个指针p令其指向第一个内层数组,然后依次迭代直到ia的全部3行都处理完为止。 在这个版本的程序中,循环终止条件由end函数负责判断。虽然我们也能推断出p的类型是指向含有4个整数的数组的指针,q的类型是指向整数的指针,但是使用auto关键字我们就不必再烦心这些类型到底是什么了。 类型别名简化多维数组的指针 读、写和理解一个指向多维数组的指针是一个让人不胜其烦的工作,使用类型别名能让这项工作变得简单一点儿: 程序将类型“4个整数组成的数组”命名为int_array,用类型名int_array定义外层循环的控制变量让程序显得简洁明了。 string和vector是两种最重要的标准库类型。string类型是一个可变长的字符序列。vector对象是一组同类型对象的容器。 练习3.1 使用恰当的using声明重做1.4.1节和2.6.2节的练习。 使用using声明重做1.4.1节中练习1.10的程序如下: 使用using声明重做1.4.1节中练习1.11的程序如下: 使用using声明重做2.6.2节中练习的程序如下: 练习3.2 编写一段程序从标准输入中一次读入一整行,然后修改该程序使其一次读入一个词。 练习3.3 请说明string类的输入运算符和getline函数分别是如何处理空白字符的 练习3.4 编写一段程序读入两个字符串,比较其是否相等并输出结果。如果不相等输出较大的那个字符串。改写程序,比较输入的两个字符串是否等长,如果不等长,输出较长的那个字符串。 练习3.5 编写一段程序从标准输入中读入多个字符串并将它们连接起来,输出连续的大字符串。修改上述程序,用合格把输入的多个字符串分隔开来。 练习3.6 编写一段程序,使用范围for语句将字符串内的所有字符都用X代替。 应该注意两点:1.应该用auto关键字让编译器推断字符串中每个字符的类型。2.c必须定义为引用类型,否则无法修改字符串的内容。 练习3.7 就上一题的程序而言,如果将循环控制变量设为char将会产生什么错误?先估计一下结果,然后实际编程进行验证。 练习3.8 分别用while循环和传统的for循环重写第一题的程序,你觉得哪种形式更好呢?为什么? 在这个例题中,我们希望改变字符串中的每个字符,且无须在意字符的处理顺序,因此与传统的while循环和for循环相比,使用范围for循环更简洁直观。 解答: 练习3.10 编写一段程序,读入一个包含标点符号的字符串,将标点符号去除后输出字符串剩余部分。 思路2 练习3.11 下面的范围for语句合法吗?如果合法,c的类型是什么? 解答: 该程序编译执行后会报错: 练习3.12 下列vector对象的定义有不正确的吗?如果有,请指出来。对于正确的,描述其执行结果;对于不正确的,说明其错误的原因。 解答: 练习3.13 下列的vector对象各包含多少个元素?这些元素的值分别是多少? 解答: 练习3.14 编写一段程序,用cin读入一组整数并把它们存入一个vector对象。 练习3.15 改写上题程序,不过这次读入的是字符串。 练习3.16 编写一段程序,把练习3.13中vector对象的容量和具体内容输出出来。检验你之前的回答是否正确,如果不对,回过头重新学习3.3.1节直到弄明白错在哪里。 练习3.17 从cin读入一组词并把它们存入一个vector对象,然后设法把所有词都改写成大写形式。输入改变后的结果,每个词占一行。 练习3.18 下面的程序合法吗?如果不合法,你准备如何修改? 解答: 练习3.19 如果想定义一个含有10个元素的vector对象,所以元素的值都是42,请列举出三种不同的实现方法。哪种方法更好?为什么? 显然思路四采取的初始化方式在形式上最简单直观。当vector对象元素较多且取值重复时,是最好的选择,而思路一在开始的时候不限定元素的个数,之后可以随意添加,较为灵活。 练习3.20 读入一组整数,并把它们存入一个vector对象,将每对相邻整数的和输出出来。改写你的程序,这次要求先输出第一个和最后一个元素的和,接着输出第二个和倒数第二个元素的和,依次类推。 练习3.21 请使用迭代器重做3.3.3节的第一个练习。 练习3.22 修改之前那个输出text第一段的程序,首先把text的第一段全部改成大写形式,然后再输出它。 练习3.23 编写一段程序,创建一个含有10个整数的vector对象,然后使用迭代器将所有元素的值都变成原来的两倍。输出vector对象的内容,检验程序是否正确。 补充: 要取得 [a,b] 的随机整数,使用 (rand() % (b-a+1))+ a; 要取得 (a,b] 的随机整数,使用 (rand() % (b-a))+ a + 1; 通用公式: a + rand() % n;其中的 a 是起始值,n 是整数的范围。 要取得 a 到 b 之间的随机整数,另一种表示:a + (int)b * rand() / (RAND_MAX + 1)。 要取得 0~1 之间的浮点数,可以使用 rand() / double(RAND_MAX)。 练习3.24 请使用迭代器重做3.3.3节的最后一个练习。 求解本题时应该特别注意迭代器begin和end的含义,其中begin指向容器的首元素而end指向容器的最后一个元素的下一个位置。 练习3.25 3.3.3节第93页划分分数段的程序是使用下标运算符实现的,请利用迭代器改写程序并实现完全相同的功能。 练习3.26 在100页的二分搜索中,为什么用的是mid=beg+(end-beg)/2,而不是mid=(beg+end)/2? 练习3.27 假设text_size是一个无参数的函数,它的返回值是int,请回答下列哪个定义是非法的?为什么? 解答: 练习3.28 下列数组元素的值是什么? 解答: 练习3.29 相比vector来说,数组有哪些缺点,请列举一些。 练习3.30 指出下面代码中的索引错误。 解答: 练习3.31 编写一段程序,定义一个含有10个int的数组,令每个元素的值就是其下标值。 练习3.32 将上一题刚刚创建的数组拷贝给另外一个数组,利用vector重写程序,实现类似的功能。 练习3.33 对于104页的程序来说,如果不初始化scores将会发生什么? 练习3.34 假定p1和p2指向同一个数组的元素,那么下面程序的功能是什么?什么情况下该程序是非法的? 解答: 练习3.35 编写一段程序,利用指针将数组中的元素置为0。 练习3.36 编写一段程序,比较两个数组是否相等。再写一段程序,比较两个vector对象是否相等。 练习3.37 下面的程序是何含义,程序的输出结果是什么? 解答: 练习3.38 在本节中我们提到,将两个指针相加不但是非法,而且也没有什么意义。请问为什么两个指针相加没有意义? 练习3.39 编写一段程序,比较两个string对象,再编写一段程序,比较两个C风格字符串的内容。 编写3.40 编写一段程序,定义两个字符数组并用字符字面值初始化它们;接着再定义一个字符数组存放前两个数组连接后的结果。使用strcpy和strcat把前两个数组的内容拷贝到第三个数组中。 练习3.41 编写一段程序,用整型数组初始化一个vector对象。 练习3.42 编写一段程序,将含有整数元素的vector对象拷贝给一个整型数组。 练习3.43 编写3个不同版本的的程序,令其都能输出ia的元素。版本1使用范围for语句管理迭代过程;版本2,3都使用普通的for语句,其中版本2要求用下标运算符,版本3要求用指针。此外,在所有3个版本的程序中都要直接写出数据类型,而不能使用类型别名、auto关键字或decltype关键字。 练习3.44 改写上个练习中的程序,使用类型别名代替控制变量的类型。 练习3.45 再次改写程序,这次使用auto关键字。
1.如果两个string对象的长度不同,而且较短string对象的每个字符都与较长的string对象对应位置上的字符相同,就说较短string对象小于较长string对象。
2.如果两个string对象在某些对应位置上不一致,则string对象比较的结果其实是string对象中第一对相异字符比较的结果。
在设计标准库类型时都力求在易用性向内置类型对其,因此大多数库类型都支持赋值操作。对于string类来说,允许把一个对象的值赋给另外一个对象:string str1(10,'c'),str2;
str1=str2;//此时str1和str2都是空串
string s1="hello, ",s2="world\n";
string s3=s1+s2;//s3的内容是hello,world\n
s1+=s2;//等价于s1=s1+s2
string s1="hello",s2="world";//都没有标点
string s3=s1+","+s2+'\n';
string s4=s1+",";//正确
string s5="hello"+", ";//错误,两个运算对象都不是string
string s6=s1+", "+"world";//正确,第一个加号左侧有string对象,运算结果为string
//第二个+号左侧是string,可以看成(s1+", ")+"world
string s7="hello" +", "+s2;//错误,前面+号左右两侧都不是string,不能把字字面值直接相加
3.2.3 处理string对象的字符
建议:使用C++版本的C标准库头文件
C++标准库中除了定义C++语言特有的功能外,也兼容了C语言的标准库。C语言的头文件形如name.h,C++则将这些文件命名为cname,而在name之前添加字母c则表示属于C语言标准库的头文件。
因此cctype头文件和ctype.h头文件内容是一样的,只不过从命名规范上来说更符合C++语言的要求。特别的,在名为cname的头文件中定义的名字从属于命名空间std,而定义在名为name.h的头文件中则不然。
一般来说,C++程序应该使用名为cname的头文件而不使用name.h的形式,标准库中的名字总能在命名空间std中找到。如果使用.h形式的头文件,程序员就不得不时刻牢记哪些是从C语言那儿继承过来的,哪些又是C++语言所独有的。
如果想对string对象中的每个字符进行操作,最好的办法是使用C++11新标准提供的语句:范围for(range for)语句。这种语句遍历给定序列中每个元素并对序列中的每个值执行某种操作,语法形式为:for(declaration:expression)
statement
一个string对象表示一个字符的序列,因此string对象可以作为范围for语句中的expression部分。举一个简单的例子来把string对象的每个字符一行一行的输出:string str("some string");
for(auto c:str)
cout<<c<<endl;//输出当前字符,后面紧跟一个换行符
使用范围for语句和ispunct函数来统计string对象中标点符号的个数:string s("Hello World!!!");
decltype(s.size()) punct_cnt = 0;//punct_cnt的类型和s.size的返回类型一样
//参见2.5.3节
for(auto c:s)
if(ispunct(c))//如果该字符是标点符号
++punct_cnt;//将计数值加一
cout<<punct_cnt<<"punctuation charaters in "<<s<<endl;
下面这个例子我们将把字符串改写为大写字母的形式。为了达到这个目的,我们可以使用标准库函数toupper,该函数接收一个字符,然后输出其对应的大写形式。这样为了把整个string对象转换成大写,只要对其中的每个字符调用toupper函数并将结果再赋给原字符就可以了:string s("Hello World!!!");
for (auto &c :s)//对于s中的每个字符,(注意c是引用)
c=toupper(c);//因为c是一个引用,因此赋值语句将改变s中字符的值
cout<<s<<endl;
c=toupper(c);//因为c是一个引用,因此赋值语句将改变s中字符的值
当执行这句语句时,实际上改变了c绑定的字符的值。
要想访问string对象中的单个字符有两个方式:一种是使用下标,另外一种是使用迭代器,其中关于迭代器的内容将在3.4节和第9章介绍。
下标运算符([ ])接收的输入参数是string::size_type类型的值,这个参数表示要访问的字符的位置;返回值是该位置上字符的引用。
string对象的下标从0计起,最大值为s.size()-1。string对象的下标必须大于等于0而小于s.size。如果超出这个范围,将引发不可预见的错误,由此推断,使用下标访问空string对象也会引发不可预知的错误。
下标的值被称为下标或索引,任何表达式中只要它的值是一个整型值就能作为索引。不过,如果某个索引是带符号类型的值将自动转换成由string::size_type表达的无符号类型。 下面的程序使用下标运算符输出string对象中的第一个字符:if(!s.empty())//确保的确有字符可以输出
cout<<s[0]<<endl;
只要我们的字符串不是常量,就能为下标运算符返回的字符赋新值。下面这个程序将字符串的首字符改写为大写形式:string s("some string");
if(!s.empty())
s[0]=toupper(s[0]);
另一个例子是把s的第一个单词改成大写形式://依次处理s中的字符直到我们处理完全部字符或者遇到一个空白
for(decltype(s.size()) index=0;index!=s.size()&& !isspace(s[index]);++index)
//一个较长的for循环:for(int i=0;i<=10;i++)
s[index] = toupper(s[index]);//将当前字符改写成大写
for语句的条件部分涉及到一点新的知识,该条件使用逻辑与运算符(&&)。如果参与运算的两个运算对象都为真,则逻辑与结果为真,否则为假。对于这个运算符来说最重要的一点是,C++语言规定只有当左侧运算对象为真时才会检查右侧运算对象的情况。 如本例所示,这条规定确保了只有当下标取值在合理范围之内时才会真的用此下标区访问字符串。也就是说,只有在index达到s.size()之前才会执行s[index]。随着index的增加,它永远也不可能超过s.size()的值,所以可以确保index比s.size()小。
要保证下标在其合理范围内,一种简单易行的方法是,总是预设下标的类型为string::size_type,因为此类型是无符号数,可以确保下标不会小于0.此时,代码只需要保证下标小于size()的值就可以了。在上段代码中就预设index和s.size()的类型一致,也就是string::size_type。C++标准并不要求标准库检测下标是否合法,一旦使用了超出范围的下标,就会产生不可预知的结果。#include
最后再次提醒,无论何时用到字符串的下标都要注意检查其合法性。在上面的程序中,我们声明的n变量是string::size_type,也就是无符号类型,可以保证n大于等于0,之后再用if语句判断n是否小于hexdigits的长度。3.3 标准库类型vector
同string一样,要想使用它就必须包含它的头文件:#inclued <vector>
using namespace std;
模板本身并不是类或函数,相反我们可以把模板看成编译器生成类或函数的一份说明。编译器根据模板创建类或函数的过程称为实例化(instantiation),当使用模板时,需要指出编译器应把类或函数实例化成何种类型。
对于类模板来说,我们通过提供一些额外信息来指定模板到底实例化成什么样的类,需要提供哪些信息由模板决定。提供信息的方式总是这样:即在模板名字后面跟一对尖括号,在括号里放上信息。
以vector为例,提供的额外信息是vector内所存放对象的类型:vector<int> ivec;//ivec保存int类型的对象
vector<Sales_item> Sales_vec;//保存Sales_item类型的对象
vector<vector<string>> file;//file这个向量的元素是一个存放string元素的vector对象
vector
。vector能容纳绝大多数类型的对象作为其元素,但是因为引用不是对象,所以不存在包含引用的vector。除此之外,其他大多数内置类型和类类型都可以构成vector对象,甚至组成vector的元素也可以是vector。需要指出的是,在早期版本的C++标准中如果vector的元素还是vector(或其他模板类型),则其定义的形式与现在C++11新标准略有不同。过去,必须在外层vector对象的右尖括号和其元素类型之间添加一个空格,如应该写成vector
,而非vector
。3.3.1 定义和初始化vector对象
vector<T> v1;//v1是一个空vector,它潜在的元素是T类型的,执行默认初始化
vector<T> v2(v1);//v2中包含v1所以元素的副本
vector<T> v2=v1;//等价于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...};//等价于v5{a,b,c...}
当然我们也可以在定义vector对象时指定元素的初始值。例如,允许把一个vector对象的元素拷贝给另一个vector对象。此时新的vector对象的元素是原vector对象的副本,注意两个vector对象的类型必须相同:vector<int> ivec;//初始状态为空
//在此处给ivec添加一些值
vector<int> ivec2(ivec);//把ivec的元素拷贝给ivec2
vector<int> ivec3=ivec;//把ivec的元素拷贝给ivec3
vector<string> svec(ivec2);//错误,svec的元素是string对象,不是int,类型不同
之前我们介绍过,C++11新标准中还提供了另外一种为vector对象的元素赋初值的方法,即列表初始化。此时用花括号括起来的0个或多个初始元素值被赋给vector对象:vector
之前我们讲过,C++提供几种不同的初始化方式(2.2.1节)。在大多数情况下,这些初始化方式可以相互等价地使用,不过也并非一直如此。目前已经介绍过的两种特殊情况是:1.使用拷贝初始化时(即使用=)(3.2.1节)只能提供一个初始值。2.如果提供的是一个类内初始值(2.6.1节)则只能使用拷贝初始化或使用花括号的形式初始化。下面我们介绍第三种特殊要求:3.如果提供的是初始值元素的列表,则只能把初始值都放在花括号里进行列表初始化,而不能放在圆括号里:vector<string> v1{"a","the","an"};//正确,列表初始化
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对象中的元素类型决定,如果是某种内置类型,如int类型时,元素初始值会自动设为0,如果是某种类类型,如string,则元素由类默认初始化:vector
对这种初始化方式有两个特殊限制:
1.有些类要求必须明确的提供初始值(2.2.1节),如果vector对象中的类型不支持默认初始化,我们就必须提供初始的元素值。对于这种类型的对象来说,只提供元素的数量而不设定初始值就无法完成初始化工作。
2.如果只提供了元素的数量而没有设定初始值,只能使用直接初始化,7.5.4节将对这一点做更详细的介绍:vector<int> vi=10;//错误,不能使用拷贝初始化,必须使用直接初始化的形式指定向量大小
在某些情况下,初始化的真实含义依赖于传递初始值时使用的是花括号还是圆括号。例如,用一个整数来初始化vector
时,整数的含义可能是vector对象的容量也可能是元素的值。类似的,如果用两个整数来初始化vector
时,这两个整数可能一个是vector对象的容量,一个是元素的初值,也可能它们是容量为2的vector对象中两个元素的初值。我们可以通过花括号或圆括号来区分上述定义:vector<int> v1(10);//v1有10个元素,每个的值都是0
vector<int> v2{10};//v2有一个元素,该元素的值是10
vector<int> v3(10,1);//v3有十个元素,每个值都是1
vector<int> v4{10,1};//v4有两个元素,值分别是10和1
另一方面,如果初始化使用了花括号形式但是提供的值又不能用来列表初始化,就要考虑用这样的值来构造vector对象了。例如,要想列表初始化含有string对象的vector对象,应该提供能赋给string对象的初值。此时就不难区分到底是要列表初始化vector对象的元素还是用给定的容量值来构造vector对象:vector<string> v5{"hi"};//列表初始化,v5有一个元素
vector<string> v6("hi");//错误,不能用字符串字面值构建vector对象
vector<string> v7{10};//v7有十个默认初始化的元素
vector<string> v8{10,"hi"};//v8有十个值为“hi”的元素
3.3.2 向vector对象中添加元素
假设我们想把创建一个vector对象包含0-999,这时候我们就应该先创建一个空vector,然后在运行时再利用vector的成员函数push_back向其中添加元素。 push_back负责把一个值当成vector对象的尾元素压到(push)到vector对象的尾端。例如:string word;
vector<string> text:
while(cin>>word)
text.push_back(word);
因为我们能够便捷高效地向vector对象中添加元素,很多编程工作被极大简化了。然而,这种便捷性也伴随着一些对编写程序更高的要求:其中一条就是必须要确保所写的循环正确无误,特别是在循环有可能改变vector对象容量的时候。
随着对vector的更多使用,我们还会逐渐了解到其他一些隐含的要求,其中一条是现在就要指出的:如果循环体内部包含向vector对象添加元素的语句,则不能使用范围for语句,具体原因在5.4.3节进行详细解释。3.3.3 其他vector操作
v.empty();//如果v不含有任何元素,返回真;否则返回假
v.size();//返回v中元素个数
v.push_back(t);//向v尾端添加一个值为t的元素
v[n];//返回v中第v个位置的引用
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)//对于v中每个元素(注意:i是一个引用)
i*=i;//求元素值的平方
for(auto i:v)
cout<<i<<" ";
cout<<endl;
vector<int>::size_type;//正确
vector::size_type;//错误
另外我们还需注意,只有当元素的值可以比较时,vector对象的值才能被比较,一些类,如string等,确实定义了自己的相等性运算符和关系运算符;另外一些,比如我们之前用过的Sales_item类支持的运算已经全部罗列在1.5.1节中了,显然并不支持,所以不能比较。
计算vector内对象的索引
只要vector对象不是一个常量,我们就能像string一样向下标运算符返回的元素赋值。
假设我们想解决 一个问题:有一组成绩的集合,取值从0到100,以10分为一个分段,要求统计各个分数段各有多少成绩。显然,有101种可能的成绩,我们把它分成11个分数段。按照上面的描述,如果输入的成绩如下:42 65 95 100 39 67 95 78 88 76 83 92 76 93
0 0 0 1 1 0 2 3 2 4 1
#include
scores[grade/10]++
就像我们之前所说的,使用下标的时候必须清楚它是否在合理范围之内。在这个程序中,我们事先确认了输入的成绩确实在0到100之内,这样计算出来的下标就一定在0到10以内。
刚接触C++语言的程序员也许会认为可以通过vector对象的下标形式来添加元素,事实并非如此。下面的代码试图为vector对象添加10个元素:vector<int> ivec;//空vector对象
for(decltype(ivec.size()) ix=0;ix!=10;++ix)
ivec[ix]=ix;//严重错误,ivec实际上不包含任何元素
for(decltype(ivec.size() ix=0;ix!=10;++ix)
ivec.push_back(ix);//正确,在ivec尾部添加一个新元素
3.4 迭代器介绍
3.4.1 使用迭代器
//由编译器决定b和e的类型;参见2.5.2节
//b表示v的第一个元素,e表示v尾元素的下一个位置
auto b=v.begin(),e=v.end();//b和e的类型相同
一般来说,我们不清楚(不在意)迭代器准确的类型是什么。在上面的例子中,使用auto关键字定义变量b和e,这两个变量的类型也就是begin和end的返回类型,第97页将对相关内容做更详细的介绍。
下面列举了迭代器支持的一些运算。使用==和!=来比较两个合法的迭代器是否相等,如果两个迭代器指向的元素相同或者都是同一个容器的尾后迭代器,则它们相等,否则不等。*iter;//返回迭代器iter所指元素的引用
iter->mem;//解引用iter并获取该元素的名为mem的成员,等价于(*iter).mem
--iter;//令iter指示容器中的上一个元素
++iter;//令iter指示容器中的下一个元素
iter1==iter2;//判断两个迭代器是否相等或不等,如果两个迭代器指示的是同一个元素
iter1!=iter2;//或它们是同一容器的尾迭代器,则相等,反之不等
举个例子,3.2.3节中的程序利用下标运算符把string对象的第一个字母改成了大写,下面利用迭代器实现同样的功能:string s("some string");
if(s.begin()!=s.end()){//确保s非空
auto it=s.begin();//it表示s的第一个字符
*it=toupper(*it);//将当前字符改写成大写形式
之前我们编写过程序,将string对象的第一个单词改写为大写形式,现在利用迭代器及其递增运算符可以实现相同功能://依次处理s的字符直到我们处理完全部字符或者遇到空白
for(auto it=s.begin();it!=s.end()&&!isspace(*it);++it)
*it=toupper(*it);//将当前字符改写为大写
关键概念:泛型编程
原来使用C或Java的程序员在转而使用C++语言之后,会对for循环中使用!=而非<进行判断有些奇怪。之所以C++程序员习惯性地使用!=,是因为它们更愿意使用迭代器而非下标,因为这种编程风格在标准库提供的所有容器上都有效。(泛型)
之前说过,只有string和vector等一些标准库类型由下标运算符,而非全部如此。而所有标准库容器的迭代器都定义了==和!=,但是它们中的大多数都没定义<运算符。因此只要我们养成了使用迭代器和!=的习惯,就不用太在意用的到底是哪种容器类型。vector<int>::iterator it;//it能读写vector
重点是理解存在一种概念上相关的类型,我们认定某个类型是迭代器当且仅当它支持一套操作,这套操作使得我们能访问容器的元素或者从某个元素移动到另外一个元素。每个容器类定义了一个名为iterator的类型,该类型支持迭代器概念所规定的一套操作。
begin和end返回的具体类型由对象是否是常量决定。如果对象是常量,begin和end返回的const_iterator;如果对象不是常量,则返回iterator。vector<int> v;
const vector<int> cv;
auto it1=v.begin();//it1的类型是vector
auto it3=v.begin();//it3的类型是vector
解引用迭代器可获得迭代器所指的对象,如果该对象的类型恰好是类,就有可能希望进一步访问它的成员。例如,对于一个由字符串组成的vector对象来说,要想检查其元素是否为空,令it是该vector对象的迭代器,只需检查it所指字符串是否为空就可以了:(*it).empty();//解引用it,然后调用结果对象的empty成员
*it.empty();//错误,试图访问it的名为empty成员,但it是个迭代器,没有empty成员
为了简化上述表达式,C++语言定义了箭头运算符(->)。箭头运算符把解引用和成员访问两个操作结合在一起,也就是说,it->mem和(*it).mem表达的意思相同。
例如,假设用一个名为text的字符串向量存放文本文件中的数据,其中的元素或者是一句话或者是用于表示段落分隔的空字符串。如果要输出text中的第一段的内容,可以利用迭代器写一个循环令其遍历text,直到遇到空字符串的元素为止://依次输出text的每一行直至遇到第一个空白为止
for(auto it=text.cbegin();it!=text.cend()&&!it->empty();++it)
cout<<*it<<endl;
3.3.2节曾经介绍过,虽然vector对象可以动态地增长,但是也会有一些副作用。已知一个限制是不能在范围for循环中向vector对象添加元素。另外一个限制是任何一种可能改变vector对象容量地操作,比如说push_back,都会使该vector对象的迭代器失效。9.3.6节将详细解释迭代器是如何失效的。3.4.2 迭代器运算
string和vector的迭代器提供了更多额外的运算符,一方面可使迭代器每次移动跨过多个元素,另外也支持迭代器进行关系运算。这些所有的运算被称为迭代器运算(iterator arithmetic),其细节由下表3.7列出:iter+n;//迭代器加上一个整数仍然是一个迭代器,它指示得新位置与原来相比向前移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指向容器尾元素的下一个位置
iter-n;//迭代器减去一个整数仍然是一个迭代器,它指示得新位置与原来相比向前移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指向容器尾元素的下一个位置
iter1+=n;//迭代器加法的复合赋值语句,将iter1加n的结果赋给iter1
iter1-=n;//将iter1加n的结果赋给iter1
iter1-iter2;//两个迭代器相减的结果是它们之间的距离,也就是说,将运算符右侧的迭代器向前移动差值个元素后将得到左侧的迭代器。参与运算的两个迭代器必须指向的是同一个容器的元素或者尾元素的下一位置
>、>=、<、<=;//迭代器关系运算符,如果某迭代器指向的容器位置在另一个迭代器所指位置之前,则说前者小于后者。参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一个位置
可以令迭代器和一个整数值相加(或相减),其返回值是向前(或向后)移动了若干个位置的迭代器。执行这样的操作时,结果迭代器或者指示原vector对象(string对象)内的一个元素,或者指示原vector对象(或string对象)尾元素的下一个位置。
举个例子,下面的代码得到一个迭代器,它指向某vector对象中间位置的元素://计算得到最接近vi中间元素的一个迭代器
auto mid=vi.begin()+vi.size()/2;
if(it<mid)//处理vi前半部分的元素
使用迭代器运算的一个经典算法是二分搜索。二分搜索从有序序列中寻找某个给定的值。二分搜索从序列中间的位置开始搜索,如果中间位置的元素正好就是要找的元素,搜索完成。如果不是,假如该元素小于要找的元素,则在序列的后半部分继续搜索;假如该元素大于要找的元素,则在序列的前半部分继续搜索。在缩小的范围中计算一个新的中间元素并重复上述过程,直至最终找到了目标或者没有元素可供继续搜索。下面的程序使用迭代器完成了二分搜索://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;//否则调整搜索范围为后半部分
mid=beg+(end-beg)/2;//新的中间点
}
当进入到循环内部后,程序通过某种规则移动beg或者end来缩小搜索的范围。如果mid所指的元素要比要找的元素sought大,可推测若text含有sought则比出现在mid所指元素的前面。反之则在后面。因为已经验证过mid不是我们要找的对象,所以在接下来的搜索中不必考虑它。循环过程终止时,mid或者等于end或者指向要指的元素。如果mid等于end,说明text没有我们要找的元素。3.5 数组
3.5.1 定义和初始化内置数组
unsigned cnt=42;//不是常量表达式
constexpr unsigned sz=42;//常量表达式,见2.4.4节
int arr[10];//含有十个整数的数组
int *parr[sz];//含有42个整型指针的的数组
string bad[cnt];//错误,cnt不是常量表达式
string strs[get_size()];//当get_size()是constexpr函数时正确,否则错误
定义数组的时候必须指定数组的类型,不允许用auto关键字由初始值的列表推断类型。另外和vector一样,数组的元素应为对象,因此不存在引用的数组。
我们可以对数组的元素进行列表初始化,此时允许忽略数组的维度。如果在声明时没有指明维度,编译器会根据初始值的数量计算并推测出来。 相反如果指明了维度,那么初始值的总数量不应该超过指定的大小。如果维度比提供的初始值数量大,则用提供的初始化靠前的元素,剩下的初始化为默认值:const unsigned sz=3;
int ial[sz]={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','+','+'};//列表初始化,没有空字符
char a2[]={'c','+','+','\'};//列表初始化,含有显式的空字符
char a3[]="C++";//自动添加表示字符串结束的空字符
const char a4[6]="Daniel";//错误,没有空间存放空字符
不能将数组的内容拷贝给其他数组作为其初始值,也不能为其他数组赋值。但一些编译器支持数组的赋值,这就是所谓的编译器扩展(compilerextension)。但一般来说,最好避免使用非标准特性,因为含有 非标准特性的程序很可能在其他编译器上无法正常工作。
和vector类型一样,数组能存放大多数类型的对象。例如可以定义一个存放指针的数组。又因为数组本身就是对象,所以允许定义数组的指针和数组的引用。在这几种情况中,定义存放指针的数组比较简单和直接,但是定义数组的指针或数组的引用就稍微复杂一点了:int *ptrs[10];//ptrs是含有10个整型指针的数组
int &refs[10]=...;//错误不存在引用的数组
int (*Parray)[10]=&arr;//parry指向一个含有10个整数的数组
int (&arrRef)[10]=arr;//arrRef引用一个含有10个整数的数组
但对于Parray来说,从右向左理解就有点不合理了。因为数组的维度是紧跟着被声明的名字,所以就数组而言,由内向外要比从右向左好多了。由内向外的顺序可帮助我们更好地理解Parray的含义:首先是圆括号括起来的部分,*Parray意味着Parray是一个指针,接下来观察右边,可知道Parray是个指向大小为10的数组的指针,最后观察坐标,知道数组中的元素是int。这样最终的含义就明白无误了,Parray是一个指针,它指向一个int数组,数组中包含10个元素。同理,(&arrRef)表示arrRef是一个引用,它引用的对象是一个大小为10的数组,数组中的元素类型是int。
当然,对修饰符的数量并没有特殊限制:int *(&array)[10]=ptrs;//array是数组的引用,该数组含有10个指针。
3.5.2 访问数组元素
数组除了大小固定以外,其他用法和vector基本类似。例如我们也可以用数组来实现我们记录各分段个数的需求。unsigned scores[11]={};//11个分数段初始化为0
unsigned grade;
while(cin>>grade){
if(grade<=100)
++scores[grade/10];
}
与vector和string一样,当需要遍历数组的所有元素时,最好的办法也是是由范围for语句:for(auto i:scores)
cout<<i<<" ";
cout<<endl;
检查下标的值
与vector和string一样,数组下标的检查由程序员负责。要想防止数组下标越界,除了小心谨慎注意细节,已经对代码进行彻底的测试之外,没有更好的办法。对于一个程序来说,即使能够顺利通过编译并执行,也不能肯定它不包含此类致命的错误。大多数常见的安全问题都源于缓冲区溢出错误。当数字或其他类似数据结构的下标越界并试图访问非法内存区域时,就会产生此类错误。3.5.3 指针和数组
通常情况下,使用取地址符来获取某个对象的指针,取地址符可以用于任何对象。数组的元素也是对象,对数组使用下标运算符得到该数组指定位置的元素。因此像其他对象一样,对数组的元素使用取地址符就能得到指向该元素的指针。string nums[]={"one","two","three"};//数组的元素是string对象
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);//ia2是一个指针,指向ia的第一个元素
ia2=42;//错误,ia2是一个指针,不能用int值给指针赋值
auto ia2(&ia[0]);//显然ia2的类型是int*
//ia3是一个含有10个整数的数组
decltype(ia) ia3={0,1,2,3,4,5,6,7,8,9};
ia3=p;//错误,不能用整型指针给数组赋值
ia3[4]=i;//正确,把i的值赋给ia3的一个元素
与2.3.2节介绍的内容相比,指向数组元素的指针拥有更多功能。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尾元素的下一个位置的指针
for(int *b=arr;b!=e;++b)
cout<<*b<<endl;//输出arr的元素
尽管我们可以得到尾后指针,但这种用法极易出错。为了让指针的使用更简单、更安全,C++11新标准引入了两个名为begin和end的函数,这两个函数定义在iteraotor头文件中。这两个函数和容器中的两个同名成员功能类似,不过数组毕竟不是类类型,因此这两个函数不是成员函数。正确的使用形式是将数组作为它们的参数:int ia[]={0,1,2,3,4,5,6,7,8,9};
int *beg=begin(ia);//指向ia首元素的指针
int *last=end(ia);//指向arr尾元素的下一个位置的指针
int *pbeg=begin(arr),*pend=end(arr);
whiel(pbeg!=pend&&*pbeg>=0)
++pbeg;
指向数组元素的指针可以执行我们之前表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尾元素arr[4]
int *p=arr+sz;//使用警告,不要解引用和递增操作
int *p=arr+10;//错误,越界
和迭代器一样,两个指针相减的结果是它们之间的距离。参与运算的两个指针必须指向同一个数组当中的元素:auto n=end(arr)-begin(arr);//n的值是5,也就是arr中元素的数量
只要两个指针指向的是同一个数组的元素,或者指向该数组尾元素的下一个位置,就能利用关系运算符对其进行比较。例如,可以按照如下的方式遍历数组中的元素:int *b=arr,*e=arr+sz;
while(b<e)
++b;
int i=0,sz=42;
int *p=&i,*e=&sz;
while(p<e)//未定义的,p和e无关,因此比较毫无意义
解引用和指针运算的交互
指针加上一个整数所得的结果还是一个指针。如果指针指向了一个元素,则允许解引用该结果指针:int ia[]={0,2,4,6,8};
int last=*(ia+4);//正确,把last初始化成8,也就是ia[4]的值。
int last=*ia+4;//错误,这是将ia解引用,再将结果加4
如前所述,在很多情况下使用数组的名字其实用的是一个指向数组首元素的指针。一个典型的例子是当对数组使用下标运算符时,编译器会自动执行上述转换操作。
对数组执行下标运算其实是对指向数组元素的指针执行下标运算: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]表示的那个元素
3.5.4 C风格字符串
字符串字面值是一种通用结构的实例,这种结构即是C++由C继承而来的C风格字符串(C-style character string)。C风格字符串不是一种类型,而是为了表达和使用字符串而形成的约定俗成的写法。按此习惯书写的字符串存放在字符数组中并以空字符结束(null-terminated)。以空字符结束的意思是在字符串最后一个字符后面跟着一个空字符(’\0’)。一般用指针来操作这些字符串。
C标准库String函数
下面的表3.8列举了C语言标准库提供的一组函数,这些函数可用于操作C风格的字符串(C语言用char数组来存放字符串),它们定义在cstring头文件中,cstring是C语言头文件string.h的C++版本。strlen(p);//返回p的长度,空字符不计算在内
strcmp(p1,p2);//比较p1和p2的相等性,如果p1==p2返回0,p1>p2返回正值,p1
传入此类函数的指针必须指向以空字符作为结束的数组:char ca[]={'c','+','+'};//不以空字符结束
cout<<strlen(ca)<<endl;//严重错误,ca没有以空字符结束
比较两个C风格字符的方法和之前学过的比较标准库string对象的方法大相径庭。比较标准库string对象时,我们用的是普通关系运算符和相等性运算符:string s1="A string example";
string s2="A different string";
if(s1<s2)//判断为false
const char ca1[]="A string example";
const char ca2[]="A difference string";
if(ca1<ca2)//未定义,试图比较两个无关地址
if(strcmp(ca1,ca2)<0)//和两个string对象的比较s1
连接和拷贝C风格字符串也与标准库string对象的同类操作差别很大。例如,要想把刚刚定义的那个string对象s1和s2连接起来,可以直接写出下面的形式:string largestr=s1+" "+s2;
正确的方法是使用strcat函数和strcpy函数。不过要想使用这两个函数还必须提供一个用于存放结果字符串的数组,该数组必须足够大以便容纳下结果字符串及末尾的空字符。下面的代码虽然很常见,但充满了风险,极易产生严重错误://如果我们计算错了largestr的大小将引发严重错误
strcpy(largestr,ca1);//把ca1拷贝给largestr
strcat(largestr," ");//把largestr的末尾加一个空格
strcat(largestr,ca2);//把ca2连接到largestr后面
3.5.4 与旧代码的接口
在3.2.1节我们介绍过允许使用字符串字面值来初始化string对象:stirng s("Hello world");//s的内容是Hello World
1.允许使用以空字符结束的字符数组来初始化string对象或为string对象赋值。
2.在string对象的加法运算中允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个运算对象都是)
3.在string对象的复合赋值运算中允许使用以空字符结束的字符数组作为右侧的运算对象。
上述性质反过来就不成立了:如果程序的某处需要一个C风格字符串,无法直接用string对象来代替它。例如,不能用string对象直接初始指向字符的指针。为了完成该功能,string专门提供了一个名为c_str的成员函数:char *str =s;//错误不能用string对象来初始化char*
const char *str=s.c_str();//正确
3.5.1介绍过不允许使用一个数组为另一个内置类型的数组赋初值,也不允许使用vector对象初始化数组。相反的,我们允许使用数组来初始化vector对象。要想实现这一目的,只需指明要拷贝区域的首元素地址和尾后地址就可以了:int int_arr[]={0,1,2,3,4,5};
vector<int> ivec(begin(int_arr),end(int_arr));//ivec有6个元素,分别是int_arr中对应元素的副本
用于初始化vector对象的值,也可以仅是数组的一部分:vector<int> subvec(int_arr+1,int_arr+4);//int_arr代表指向这个数组首元素的指针
//这条初始化语句用三个元素创建了对象的subvec,分别是int_arr[1],int_arr[2],int_arr[3]
现代的C++程序应当尽量使用vector和迭代器,避免使用内置数组和指针;应该尽量使用string,避免使用C风格的基于数组的字符串。3.6 多维数组
当一个数组的元素仍然是数组时,通常使用两个维度来定义它:一个表示数组本身的大小,另一个表示其元素(也是数组)的大小:int ia[3][4];//大小为3的数组,每个元素是含有4个整数的数组
int arr[10][20][30];//arr是一个大小为10的数组,其每个元素都是大小为20的数组,这些数组的元素是含有30个整数的数组。
int ia[3][4]={//三个元素,每个元素都是大小为4的数组
{0,1,2,3},//第一行的初始值
{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}};//其他未列出的元素执行默认值初始化,这个过程和一维数组一样。
//显式地初始化第一行,其他元素执行值初始化
int [3][4]={0,3,6,9};//它初始化的是第一行的4个元素,其他元素被初始化为0
可以使用下标运算符来访问多维数组的元素,此时数组的每个维度对应一个下标运算符。
如果表达式含有下标运算符的数量和数组的维度一样多,该表达式的结果将是给定类型的元素;反之,如果表达式含有的下标运算符数量比数组的维度小,则表达式的结果将是给定索引值处的一个内层数组://用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];//12个未初始化的元素
for(size_t i=0;i!=cowcnt;++i){//对于每一行
for(size_t j=0;j!=colcnt;++j){//对于行内的每一列
ia[i][j]=i*colcnt+j;//将元素的位置索引作为它的值
}
}
由于在C++11新标准中新增了范围for语句,所以前一个程序可以简化为:size_t cnt=0;
for(auto &row:ia){
for(auto &col:row){
col=cnt;
++cnt;
}
}
在上面的例子中,因为我们要改变数组元素的值,所以我们选用引用类型作为循环控制变量,但其实还有一个深层次的原因促使我们这么做。举一个例子,考虑如下的循环:for(const auto &row:ia)
for(auto col:row)
cout<<col<<endl;
for(auto row:ia)
for(auto col:row)
定义指向多维数组的指针时,别忘了这个多维数组实际上是数组的数组。所以由多维数组名转换得来的指针实际上是指向第一个内层数组的指针:int ia[3][4];
int (*p)[4]=ia;//p指向含有4个整数的数组
p=&ia[2];//p指向ia的尾元素(最后一个含有4个整数的数组)
int *ip[4];
这个语句的含义其实是存放4个整型指针的数组。
随着C++11新标准的提出,通过使用auto或者decltype就能尽可能地避免在数组前面加上一个指针类型了://输出ia中每个元素的值,每个内层数组各一行
//p指向含有4个整数的数组
for(auto p=ia;p!=ia+3;++p){
for(auto q=*p;q!=*p+4;++q)
cout<<*q<<' ';
cout<<endl;
}
内层的for循环负责输出内层数组所包含的值。它首先令指针q指向p当前所在行的第一个元素。*p是一个含有4个整数的数组(是一个数组名),像往常一样,数组名被自动地转换成指向该数组首元素的指针。
当然,使用标准库函数begin和end也能实现同样的功能,而且看起来更简洁一些://p指向ia的第一个数组
for(auto p=begin(ia);p!=end(ia);++p){
//q指向内层数组的首元素
for(auto q=begin(*p);q!=end(*p);++q)
cout<<*q<<' '
cout<<endl;
}
using it_array=int[4];//新标准下类型别名的声明,参见2.5.1节
typedef int int_array[4];//等价的typedef声明,参见2.5.1节
//输出ia中每个元素的值,每个内层数组各占一行
for(int_array *p=ia;p!=ia+3;++p){
for(int *q=*p;q!=*p+4;++q)
cout<<*q<<' ';
cout<<endl;
}
小结
迭代器允许对容器中的对象进行间接访问,对于string对象和vector对象来说,可以通过迭代器访问运算或者在元素中移动。
数组和指向数组的指针在一个较低的层次上实现了与标准库类型string和vector类似的功能。一般来说,应该优先使用标准库提供的类型,之后再考虑C++语言内置的低层的替代品数组和指针。习题解答
3.1节练习
解答:
使用using声明重做1.4.1节中练习1.9的程序如下:#include
#include
#include
#include
3.2.2节练习
解答:#include
#include
解答:
标准库string输入运算符自动忽略字符串开头的空白(包含空格符,换行符,制表符等),从第一个真正的字符开始读起,直到遇到下一个字符为止。
如果希望在最终的字符串中保留输入的空白符,应该使用getline函数代替原来的>>运算符,getline从给定的的输入流中读取数据,直到遇到换行符为止,此时换行符也被读进来,但并不存储在最后的字符串中。一个典型的示例如下:#include
解答:#include
#include
解答:#include
#include
3.2.3节练习
解答:#include
解答:
应该不会产生错误。因为上一题用auto关键字推断出来的类型本来就是char。#include
解答:#include
#include
练习3.9 下面的程序有何作用?它合法吗?如果不合法,为什么?string s;
cout<<s[0]<<endl;
本题的原意是输出字符串s的首字符,但程序是错误的。因为s是默认初始化,为空串,当然不存在首字符,下标0是非法的。
解答:
思路1#include
#include
const string s="Keep out!";
for(auto &c:s){/*....*/}
该程序从语法上来说是合法的,s是一个常量字符串,则c的推断类型为常量引用,即c所绑定的对象值不能被改变。为证明这一点,我们可以编一个程序验证:#include
9 4 D:\c++学习\test.cpp [Error] assignment of read-only reference 'c'
3.3.1节练习
vector<vector<int>> ivec;
vector<string> svec=ivec;
vector<string> svec=(10,"null");
第一行是正确的,定义了一个名为ivec的vector,其每个元素都是一个vector
对象。第二行是错误的,svec的元素类型是string,而ivec的元素类型是int,因此不能使用ivec初始化svec。第三行是正确的,定义了一个名为svec的vetor对象,其中含有十个字符串null。vector<int> v1;
vector<int> v2(10);
vector<int> v3(10,42);
vector<int> v4{10};
vector<int> v5{10,42};
vector<string> v6{10};
vector<string> v7{10,"hi"};
第一行元素个数为0。第二行元素个数为10。第三行元素个数10。第四行元素个数为1。第五行元素个数为2。第六行元素个数为10,每一个元素为空串。第七行元素个数为10个。3.3.2节练习
解答:#include
解答:#include
3.3.3节练习
解答:#include
解答:#include
vector<int> ivec;
ivec[0]=42;
不合法,因为初始化之后vector没有任何元素,ivec[0]的形式是错误的。要想添加新元素应该使用push_back()函数。vector<int> ivec;
ivec.push_back(42);
解答:
1.vector<int> vint;//定义空vector,再for循环添加元素
for(int i=0;i<10;i++)
vint.push_back(42);
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);//用括号给出所有元素的值,类似思路2
vector<int> vint(10,42);//定义时使用参数指定元素个数及重复的值
vector<int> vint(10);//先指定元素个数,再利用范围for循环依次为元素赋值
for(auto &i:vint)
i=42;
解答:#include
#include
3.4.1节练习
解答:#include
解答:#include
#include
产生一定范围随机数的通用表示公式
要取得 [a,b) 的随机整数,使用 (rand() % (b-a))+ a;3.4.2节练习
解答:#include
#include
解答:#include
解答:
C++并没有定义两个迭代器的加法运算,实际上直接把两个迭代器相加是没有意义的。
与之相反,C++定义了迭代器的减法运算,两个迭代器相减的结果是他们之间的距离,也就是说,将运算符右侧的迭代器向前移动多少个元素后可以得到左侧的迭代器。
另外C++还定义了迭代器与整数的加减法运算,用来控制迭代器在容器中左右移动。
在本题中,因为迭代器相加法不存在,所以mid=(beg+end)/2;不合法。mid=beg+(end-beg)/2;的含义是,先计算end-beg的值得到容器的容量个数,然后控制迭代器从开始处向右移动二分之一容器的长度,从而定位到容器正中间的位置。3.5.1节练习
unsigned buf_size =1024;
int ia[buf_size];
int ia[4*7-14];
int ia[text_size()];
char st[11]="fundamental";
第一行非法,因为buf_size是一个普通的无符号整数,数组的维度只能是常量,所以不能作为数组的维度。第二行合法,因为中括号中的表达式是一个常量表达式。第四行非法,因为text_size()是一个普通的函数调用,没有被定义为constexpr,不能作为数组的维度。第四行非法,这个字符加上空字符共有12个字符,但是字符数组st的维度只有11,无法容纳。string sa[10];
int ia[10];
int main(){
string sa2[10];
int ia2[10];
}
本题在考察数组默认初始化的几种给不同情况,如全局变量和局部变量的区别、内置类型和复合类型的区别。
对于string数组来说**,因为string类本身就接收无参数的初始化方式,所以不论数组定义在函数内还是函数外都被默认初始化为空串。**
对于内置类型int来说,数组ia定义在所有函数体之外,ia的所有元素默认初始化为0;而数组ia2定义在main函数的内部,将不被初始化,如果程序试图拷贝或输出未初始化的变量,将遇到未定义的奇异值。
解答:
数组与vector相比有一些类似之处:都能存放相同类型的对象,且这些对象本身并没有名字,需要通过其所在位置进行访问。
数组与vector最大的不同时,数组的大小固定不变,不能随意向数组中增加额外的元素,虽然有些情境下运行性能较好,但与vector相比损失了灵活性。
具体来说,数组的维度在定义时就已经确定,如果我们想更改数组的长度,只能创建一个更大的新数组,然后把原数组的所有元素复制到新数组中。我们也无法像vector那样使用size函数直接获取数组的维度。如果是字符数组,可以调用strlen函数得到字符串的长度;如果是其他数组,只能使用sizeof(array)、sieof(array[0])的方式计算数组的维度。3.5.2节练习
constexpr size_t array_size=10;
int ia[array_size];
for(size_t ix=1;ix<=array_size;++ix)
ia[ix]=ix;
本题的原意是创建一个大小为10的数组存放它们下标值。而for循环从1开始,10结束,超出了ia数组的下标范围(0-9)。正确的写法为:constexpr size_t array_size=10;
int ia[array_size];
for(size_t ix=0;ix<array_size;++ix)
ia[ix]=ix;
解答:#include
解答:
如果想把数组的内容拷贝给另外一个数组,不能直接对数组使用赋值运算符,而是要逐一拷贝数组的元素。vector拷贝的原理和数组类似。#include
#include
解答:
该程序对scores执行了列表初始化,为所有元素赋初值为0,这样在后续的统计时将会从0开始计算各分段的人数,是正确的做法。
如果不初始化scores,则数组会含有未定义的值,这是因为scores是定义在函数体内部的整型数组,不会执行默认初始化。3.5.3节练习
p1+=p2-p1;
如果这两个指针指向同一个数组的元素,则该条语句令p1指向原来p2所指向的元素。p2-p1得到这两个指针的距离d,p1+=d语句使p1向后移动d个元素,即到达p2原来指向的元素。
从语法上来说,即使p1和p2指向的元素不属于同一个数组,但只要p1和p2类型相同,该语法也是合法的。如果p1和p2的类型不同,则编译时报错。
解答:#include
解答:
该例类似一个彩票游戏,先由程序随机选出5个0-9的数字,此过程类似于摇奖;再由用户手动输入5个猜测的数字,类似于购买彩票;分别把两组数字存入数组a和b,然后逐一对比两个数组的元素;一旦有数不一致,则告知用户猜错了,只有当两个数组的所有元素都相等时,判定数组相等,即用户猜测正确。#include
#include
3.5.4节练习
const char ca[]={'h','e','l','l','o'};
const char *cp=ca;
while(*cp){
cout<<*cp<<endl;
++cp;
}
该程序的原意是每行输出一个字符数组中的字符。但实际执行的效果无法符合预期。因为以列表初始化方式赋值的C风格字符串与以字符串字面值赋值有所区别,字符串字面值赋值会在字符串最后增加一个空字符以示字符串的结束,而列表初始化则不会这样。所以在该程序中,ca的五个字符全部输出后并没有遇到预期的空字符,也就是说,while循环的条件仍将满足,无法跳出。程序继续在内存中ca的存储位置之后挨个寻找空字符,直到找到为止。在这个过程中,额外经历的内容也会被输出,从而产生错误。
解答:
指针也是个对象,与指针相关的属性有三个,分别是指针本身的值(value),指针所指的对象(content)以及指针本身在内存中的存储位置(address)。它们的含义分别为:指针本身的值是一个内存地址值,表示指针所指对象在内存中的存储地址;指针所指的对象可以通过解引用指针访问;因为指针也是一个对象,所以指针也存在内存中的某个位置,这就是为什么有指针的指针的原因。
通过上述的分析我们知道,指针的值是它所指对象的内存地址,如果我们想把两个指针加在一起,就是试图把内存中两个对象的地址加在一起,显然没有任何意义。相反,指针的减法是有意义的。如果两个指针指向同一个数组的不同元素,则它们相减的结果表征了它们所指的元素在数组中的距离。
解答:#include
#include
解答:#include
3.5.5节练习
解答:#include
解答:#include
3.6节练习
解答:#include
解答:#include
解答:
#include