C++ Primer 学习笔记(3)

第三章 字符串、向量和数组

3.1 命名空间的using声明

作用域操作符(::):编译器应从操作符左侧名字所示的作用域中寻找右侧那个名字。

头文件不应包含using声明,位于头文件的代码,一般来说不应该使用using声明。这是因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个using声明,那么每个使用了该头文件的文件都会有这个声明。对于某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突。

3.2 标准库类型string

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

3.2.1 定义和初始化string对象

一个类可以定义很多种初始化对象的方式。

初始化 string 对象的方式
string s1默认初始化,s1是一个空串

string s2(s1)

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

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

3.2.2 string对象上的操作

类既能定义通过函数名调用的操作,也能定义<<、+等各种运算符在该类对象上的新含义。

读写string对象

可以使用标准库的IO操作符来读写string对象。

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

和内置类型的输入输出操作一样,string对象的此类操作也是返回运算符左侧的运算对象作为其结果。因此,多个输入或者多个输出可以连写在一起。

使用getline读取一整行

有时我们希望能在最终得到的字符串中保留输入时的空白符,这时应该用getline函数代替原来的>>运算符。

getline的参数是一个输入流(cin)和一个string对象。

getline只要一遇到换行符就结束读取操作并返回结果(注意不存换行符,触发getline函数返回的那个换行符实际上被丢弃掉了),哪怕输入的一开始就是换行符也是如此。如果输入真的一开始就是换行符,那么得到的结果是个空string。

和输入运算符一样,getline也会返回它的流参数,因此也能用getline的结果作为条件。

string的empty和size操作

empty函数根据string对象是否为空返回一个对应的布尔值。

size函数返回string对象的长度(即string对象中字符的个数)。

string::size_type类型

string类及其他大多数标准库类型都定义了几种配套的类型。这些配套类型体现了标准库类型与机器无关的特性,类型size_type即是其中一种。

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

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

比较string对象

大小写敏感

相等性运算符:==、!=

关系运算符:<、<=、>、>=

这些运算符都依照字典顺序:

1、如果两个string对象的长度不同,而且较短string对象的每个字符都与较长string对象对应位置上的字符相同,就说较短string对象小于较长string对象

2、如果两个string对象在某些对应的位置上不一致,则string对象比较的结果其实是string对象中第一对相异字符比较的结果

为对象赋值

对于string类而言,允许把一个对象的值赋给另外一个对象:s1 = s2

两个string对象相加

两个string对象相加得到一个新的string对象,其内容是把左侧的运算对象与右侧的运算对象串接而成。

加法运算符(+)和复合赋值运算符(+=)

字面值和string对象相加

标准库允许把字符字面值和字符串字面值转换成string对象,所以在需要string对象的地方就可以使用这两种字面值来替代。当把string对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符(+)的两侧的运算对象至少有一个是string

C++语言中的字符串字面值并不是标准库类型string的对象。

3.2.3 处理string对象中的字符

cctype头文件中定义了一组标准库函数来处理这部分工作。

C++标准库中除了定义C++语言特有的功能外,也兼容了C语言的标准库。C语言的头文件形如name.h,C++则将这些文件命名为cname。特别的,在名为cname头文件中定义的名字从属于命名空间std,而定义在名为.h的头文件中的则不然。

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

string str("some string");
for (auto c : str) {
    cout << c << endl;
}

每次迭代,str的下一个字符被拷贝给c。

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

只处理一部分字符?

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

下标运算符([ ])接收的输入参数是string::size_type类型的值,这个参数表示要访问的字符的位置,下标的值称作“下标”或“索引”,任何表达式只要它的值是一个整型值就能作为索引。不过,如果某个索引是带符号类型的值将自动转换成由string::size_type表达的无符号类型;返回值是该位置上字符的引用

C++标准并不要求标准库检测下标是否合法。使用超出范围的下标和使用下标访问空string都是引发不可预知的后果。在访问指定字符串之前,首先检查是否为空。

除基于范围的for语句之外,也可以使用下标执行迭代。

逻辑与运算符(&&):C++语言规定只有当左侧运算对象为真时才会检查右侧运算对象的情况。

3.3 标准库类型vector

标准库类型vector表示对象的集合,其中所有对象的类型都相同,vector也常被称作容器。要想使用vector,必须包含适当的头文件,vector也定义在命名空间std中。

C++语言既有类模板,也有函数模板,其中vector是一个类模板。编译器根据模板创建类或函数的过程称为实例化,当使用模板时,需要指出编译器应把类或函数实例化成何种类型,即在模板名字后面跟一堆尖括号,在括号内放上信息。

vector能容纳绝大多数类型的对象,但不存在包含引用的vector。

3.3.1 定义和初始化vector对象

初始化vector对象的方法
vector<T> v1默认初始化,v1是一个空vector
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对象的元素是内置类型,比如int,则元素初始值自动设为0。如果元素是某种类类型,比如string,则元素由类默认初始化。

列表初始化还是元素数量?

在某些情况下,初始化的真实含义依赖于传递初始值时用的是花括号还是圆括号:

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

2、如果用的是花括号,可以表述成我们想列表初始化该vector对象。也就是说,初始化过程会尽可能地把花括号内的值当成是元素初始值的列表来处理,只有在无法执行列表初始化时才会考虑其他初始化方式。如果初始化时使用了花括号的形式但是提供的值又不能用来列表初始化,就要考虑用这样的值来构造vector对象。

vector<string> v5{"hi"};      // 列表初始化
vector<string> v6("hi");      // 错误
vector<string> v7{10};        // 正确
vector<string> v8{10, "hi"};  // 正确

3.3.2 向vector对象中添加元素

push_back负责把一个值当成vector对象的尾元素“压到”vector对象的“尾端”。

开始的时候创建空的vector对象,在运行时再动态添加元素,会比创建vector对象时就指定其容量,性能表现更好。只有一种例外情况,就是所有元素的值都一样。

如果循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环。这是因为在范围for语句中,预存了end()的值,一旦在序列中添加或删除匀速,end函数的值就可能变的无效了。

3.3.3 其他vector操作

除了push_back之外,vector还提供了几种其他操作,大多数都和string的相关操作类似。

v1 = v2;          // 用v2中元素的拷贝替换v1中的元素
v1 = {a, b, c...} // 用列表中元素的拷贝替换v1中的元素

相等性运算符:==、!=

关系运算符:<、<=、>、>=

关系运算符依照字典顺序进行比较,同string。只有当元素的值可比较时,vector对象才能被比较。

vector的size函数返回类型和下标运算符的类型都是由vector定义的size_type类型。

一种好的习惯:总是设下标的类型为size_type,因此此类型是无符号数,可以确保下标不会小于0。

只能对确知已存在的元素执行下标操作。试图用下标的形式去访问一个不存在的元素将引发错误,不过这种错误不会被编译器发现,而是在运行时产生一个不可预知的值,与string相同。这就是所谓的缓冲区一出错误,确保下标合法的一种有效手段就是尽可能使用范围for语句。

3.4 迭代器介绍

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

类似于指针类型,迭代器也提供了对对象的间接访问。迭代器的对象是容器中的元素或者string对象中的字符。

迭代器有有效和无效之分,这一点和指针差不多,有效的迭代器或者指向某个元素,或者指向容器中尾元素的下一位置,其他所有情况都属于无效。

3.4.1 使用迭代器

begin:指向第一个元素

end:指向容器(或string对象)“尾元素的下一位置”,指示的是容器的一个本不存在的“尾后元素”,仅是个标记而已。end成员返回的迭代器常被称作尾后迭代器或者尾迭代器。

特殊情况下如果容器为空,则begin和end返回的是同一个迭代器。

一般来说,我们不清楚(不在意)迭代器准确的类型到底是什么,因此我们使用auto关键字。

迭代器运算符

*:解引用。和指针类似,也能通过解引用迭代器来获取它指示的元素,执行解引用的迭代器必须合法并确实指示着某个元素。试图解引用一个非法迭代器或者尾后迭代器都是未被定义的行为。

->:解引用并获取该元素的成员

相等性运算符:==、!=

如果两个迭代器指向的元素相同或者都是同一个容器的尾后迭代器,则它们相等。

++、--:移动迭代器指向元素的位置,不能对end进行递增或者递减操作

for循环中,C++程序员习惯性地使用!=。

迭代器类型

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

vector<int>::iterator it1;
string::iterator it2;

vector<int>::const_iterator it3;
string::const_iterator it4;

const_iterator和常量指针差不多,能读取但不能修改它所指的元素值,相反,iterator的对象可读可写。如果vector对象或string对象是一个常量,只能使用const_iterator;否则,两种都能使用。

begin和end运算符

begin和end返回的具体类型由对象是否是常量决定。

如果对象只需读操作而无需写操作的话最好使用常量类型。为了便于专门得到const_iterator类型的返回值,C++11新标准引入了两个新函数,分别是cbegin和cend。

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

(*it).empty() (由于运算符优先级的问题,圆括号必不可少)简化为 it->empty()

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

1、不能在范围for循环中向vector对象添加元素

2、任何一种改变vector对象容量的操作,都会使改vector对象的迭代器失效

3.4.2 迭代器运算

string和vector的迭代器提供了更多额外的运算符,一方面可使得迭代器的每次移动跨过多个元素,另外也支持迭代器进行关系运算。

iter +(-) n:向前或向后移动若干个位置,结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一位置。

iter +=(-=) n:复合赋值语句,同上。

iter1-iter2:两个迭代器之间的距离。参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一位置。其所得结果类型是名为difference_type的带符号整数,因为这个距离可正可负,string和vector都定义了difference_type。

>、>=、<、<=:关系运算符,如果某迭代器指向的容器位置在另一个迭代器所指位置之前,则说前者小于后者。参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一位置。

使用迭代器运算的一个经典算法是二分搜索。

3.5 数组

数组是一种类似于标准库类型vector的数据结构,不同的地方时,数组的大小确定不变。因此对某些特殊的应用来说程序的运行时性能较好,但是相应的也损失了一些灵活性。

3.5.1 定义和初始化内置数组

数组的维度必须是一个常量表达式。

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

定义数组的时候必须指定数组的类型,不允许使用auto关键字。另外和vector一样,数组的元素应为对象,因此不存在引用的数组。

列表初始化(显示初始化数组)

1、允许忽略数组的维度,编译器会根据初始值的数量计算并推测出来;

2、如果指明了维度,那么初始值的总数量不应该超出指定的大小;

3、如果维度比提供的初始值数量大,则用提供的初始值初始化靠前的元素,剩下的元素被初始化成默认值,比如int就是0(这里跟未定义区分)

字符数组的特殊性

我们可以用字符串字面值对字符数组初始化。当使用这种方式时,一定要注意字符串字面值的结尾处还有一个空字符(需要多预留一个空间)。

不允许拷贝和赋值

不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值。

一些编译器支持数组赋值,这就是所谓的编译器扩展。但一般来说,最好避免使用非标准特性。

理解复杂的数组声明

数组本身就是对象,所以允许定义数组的指针和数组的引用。

默认情况下,类型修饰符从右向左依次绑定。但因为数组的维度是紧跟着被声明的名字的,所以就数组而言,最好的办法是从数组的名字开始按照由内向外的顺序阅读。

3.5.2 访问数组元素

在使用数组下标时,通常将其定义为size_t类型。size_t是一种机器相关的无符号类型,它被设计得足够大以便能表示内存中任意对象的大小。在cstddef头文件中定义了size_t类型。

与string和vector一样,试图用下标的形式去访问一个不存在的元素将引发错误,不过这种错误不会被编译器发现。

3.5.3 指针和数组

C++语言中,指针和数组有非常紧密的联系,使用数组的时候编译器一般会把它转换成指针,指向该数组首元素的指针。

使用auto推断得到指针,使用declty推断得到数组。

指针也是迭代器

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

标准库函数begin和end

由于数组不是类类型,因此这两个函数不是成员函数,正确的使用方式是将数组作为它们的参数。begin和end函数定义在iterator头文件中。

int ia[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int *beg = begin(ia);
int *last = end(ia);

end函数返回指向ia尾元素下一位置的指针。特别注意,同string和vector一样,尾后指针不能执行解引用和递增操作。

指针运算

指向指针元素的指针可以执行之前列出的所有迭代器运算,包裹解引用、递增(减)、比较、与整数相加、两个指针相减等,用在指针和用在迭代器上意义完全一致。

给指针加上一个整数,得到的新指针仍需指向同一数组的其他元素,或者指向同一数组的尾元素的下一位置。如果计算得到的指针超出了上述范围就将产生错误,而且这种错误编译器一般发现不了

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

只要两个指针指向同一个数组的元素,或者指向该数组的尾元素的下一位置,就能利用关系运算符进行比较。

上述指针运算同样适用于空指针和所指对象并非数组的指针。在后一种情况下,两个指针必须指向同一对象或者该对象的下一位置。如果是空指针,允许加上或减去一个值为0的整型常量表达式。两个空指针也允许彼此相减,结果是0。

下标和指针

对数组执行下标运算其实是对指向数组的元素的指针执行下标运算。只要指针指向的是数组中的元素,都可以执行下标运算。

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

虽然标准库类型string和vector也能执行下标运算,但是数组与它们相比还是有所不同。标准库类型限定使用的下标必须是无符号类型,而内置的下标运算无此要求。当然,结果地址必须指向原来的指针所指同一数组中的元素(或者同一数组尾元素的下一位置)。

3.5.4 C风格字符串

尽管C++支持C风格字符串,但在C++程序中最好还是不要使用它们。

字符串字面值就是一种C++由C继承而来的C风格字符串,C风格字符串不是一种类型,而是为了表达和使用字符串而形成的一种约定俗成的写法。按此习惯书写的字符串存放在字符数组中并以空字符结束,一般利用指针来操作这些字符串。

C标准库String函数

这些函数可用于操作C风格字符串,定义在cstring头文件中。

C风格字符串的函数
strlen(p)返回p的长度,空字符不计算在内
strcmp(p1, p2)比较p1和p2的相等性,如果p1==p2,返回0;如果p1>p2,返回一个正值;如果p1<p2,返回一个负值
strcat(p1, p2)

将p2附加到p1之后,返回p1

strcpy(p1, p2)将p2拷贝给p1,返回p1

上述函数不负责验证其字符串参数。传入此类函数的指针必须指向以空字符作为结束的数组,否则,程序将产生未定义的结果。strlen函数有可能沿着ca在内存中的位置不断向前寻找,直到遇到空字符才停下来。

比较字符串

比较标准库string对象的时候,用的是普通的关系运算符和相等性运算符。

如果把这些运算符用在两个C风格字符串上,实际比较的将是指针而非字符串本身。这两个指针指向的并非同一对象,所以将得到未定义的结果。

目标字符串的大小由调用者指定

连接和拷贝C风格字符串也与标准库string对象的同类操作差别很大,string的连接可以直接使用+运算符,拷贝使用=。

但是C风格字符串是使用strcat函数和strcpy函数,使用这两个函数需要提供一个用于存放结果字符串的数组,该数组必须足够大以便容纳下结果字符串及结尾的空字符串。

一个潜在的问题是,我们在估算空间时不容易估准,而且其所存内容一旦改变,就必须重新检查其空间是否足够。这类代码充满了风险而且经常导致严重的安全泄露。

对大多数应用来说,标准库string要更加安全和高效。

3.5.5 与旧代码的接口

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

1、允许使用以空字符结束的字符数组来初始化string对象或为string对象赋值

2、在string对象的加法运算中允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个运算对象都是),复合赋值运算中也允许其作为右侧的运算对象

上述性质反过来就不成立了:如果程序某处需要一个C风格字符串,无法直接使用string对象来代替它。为了完成该功能,string专门提供了一个名为c_str的成员函数,函数返回的类型是const char*指针,从而确保我们不会改变字符数组的内容。该指针指向一个以空字符结束的字符数组,而这个数组所存的数据恰好与那个string对象一样(string对象不以空字符结束)。

我们无法保证c_str函数返回的数组一直有效,事实上,如果后续的操作改变了s的值就可能让之前返回的数组失去效用。如果执行完c_str函数后程序想一直都能使用其返回的数组,最好将该数组重新拷贝一份。

使用数组初始化vector对象

不允许使用一个数组为另一个内置类型的数组赋初值,也不允许使用vector对象初始化数组。相反的,允许使用数组来初始化vector对象。要实现这一目的,只需指明要拷贝区域的首元素地址和尾后地址就可以了。

int int_arr[] = {0, 1, 2, 3, 4, 5};
std::vector<int> ivec(begin(int_arr), end(int_arr));

// 用于初始化vector对象的值也可能仅是数组的一部分
std::vector<int> subVec(int_arr + 1, int_arr + 3);
// 这条初始化语句用3个元素创建了对象subVec:int_arr[1]、int_arr[2]、int_arr[3]

现代的C++程序应当尽量使用vector和迭代器,避免使用内置数组和指针。

3.6 多维数组

严格来说,C++语言中没有多维数组,通常所说的多维数组其实是数组的数组。定义数组时,对下标运算符的数量并没有限制,按照由内(数组名)而外的顺序阅读此类定义有助于更好的理解其真实含义。

对于二维数组来说,常把第一个维度称作行,第二个维度称作列。

多为数组的初始化

可以使用列表初始化,内层嵌套着的花括号并非必需的,会从第一行第一个元素开始依次初始化,其他未列出的元素执行默认值初始化。可以通过内层嵌套的花括号来规定初始化的行。

多维数组的下标引用

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

使用范围for语句处理多维数组

要使用范围for语句处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型。否则程序无法通过编译,这是因为编译器初始化控制变量时会自动将这些数组形式的元素转换成指向该数组内首元素的指针。

指针和多维数组

由多维数组名转换得来的指针实际上是指向第一个内层数组的指针。再解引用,就得到第一个内层数组本身,即指向该数组首元素的指针。

类型别名简化多维数组的指针

using int_array = int[4];  // 新标准下类型别名的声明
typedef int int_array[4];  // 等价的typedef声明

术语表

缓冲区溢出:一种严重的程序故障,主要的原因是试图通过一个越界的索引访问容器内容,容器类型包括string、vector和数组等。

C风格字符串:以空字符结束的字符数组。字符串字面值是C风格字符串,C风格字符串容易出错。

拷贝初始化:使用赋值号(=)的初始化形式,新创建的对象是初始值的一个副本。

直接初始化:不使用赋值号(=)的初始化形式。

范围for语句:一种控制语句,可以在值的一个特定集合内迭代。

值初始化:是一种初始化过程。内置类型初始化为0,类类型由类的默认构造函数初始化。只有当类包含默认构造函数时,该类的对象才会被值初始化。对于容器的初始化来说,如果只说明了容器的大小而没有指定初始值的话,就会执行值初始化。此时编译器会生成一个值,而容器的元素被初始化为该值。

[]运算符:下标运算符。如果p是指针、n是整数,则p[n]与*(p + n)等价。

->运算符:箭头运算符。该运算符综合了解引用操作和点操作。

&&运算符:只有当左侧运算对象为真时才会检查右侧运算对象。

||运算符:只有当左侧运算对象为假时才会检查右侧运算对象。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值