第三章 字符串、向量和数组
这一章中介绍了C++语言定义的抽象数据类型库,包括string和vector等重要的标准库类型,迭代器是这两种标准库类型的配套类型,常用于访问string和vector中的数据。
在学习了第二章介绍的一些内置的类型后,我们进入了更深层次的数据类型的学习,这些类型未像数组或者字符那样直接实现到计算机硬件中。
命名空间的using声明
目前我们用到的库函数大多都来自命名空间std,在之前的学习中我们将其显式地标示了出来,如std::cin
,此处的作用域操作符::
表示编译器从操作符左侧名字琐事的作用域中寻找右侧的名字,所以std::cin
的意思就是使用命名空间std
中的名字cin
。
这种方法很繁琐,我们可以通过简单的途径使用命名空间中的成员,也就是using声明,其形式如下:using namespace::name
,在程序中使用了这条语句就可以直接访问命名空间中的名字。注意:每个名字都需要独立的using声明
#include
using std::cin;
using std::cout;
using std::endl;
int main() {
int i = 0;
cin >> i;
cout << i << endl;
return 0;
}
头文件中不应包含using声明,因为头文件的内容会拷贝到所有引用它的文件中,如果头文件中包含某个using声明,那么可能会产生名字冲突。
标准库类型string
string类型表示可变长的字符序列,使用需包含头文件string
,其作为标准库类型定义在std
命名空间中。
根据上一小节所说命名空间的声明,我们可以在文件中包含using std::string
声明,便可以直接在程序中使用string
类型而不用标示其命名空间。
定义、初始化string对象
四种初始化string对象的方式:
string s1; //默认初始化,s1是一个空字符串
string s2 = s1 //s2是s1的副本
string s2(s1) //s2是s1的副本
string s3 = "zhc"; //s3是该字符串字面值的副本
string s3("ahc")
string s4(4, 'z'); //s4的内容为zzzz
注意在第四行和第五行的初始化中,该字面值中除了最后的空字符外其他所有字符都被拷贝到新创建的string对象中,第六行中提供了一个数字和一个字符,string对象被初始化为给定字符连续重复若干次后得到的序列
直接初始化和拷贝初始化
使用等号=
初始化一个变量,实际上执行的是拷贝初始化,不使用等号则执行直接初始化
string对象的操作
读写string对象
可以使用第一章中的IO操作符读写string对象
#include
#include
using std::cin;
using std::cout;
using std::string;
int main() {
string s;
cin >> s;
cout << s << endl;
return 0;
}
在执行读写操作时,string对象会自动忽略开头的空白(空格符、换行符、制表符等),从第一个真正的字符开始读起,直到遇到下一处空白为止。如果程序输入"Hello World", 程序会输出"Hello"。string读写中,多个输入或者输出可以连写在一起。
使用getline读取一整行
使用getline读取字符串可以保留输入时的空白符,getline接收一个输入流和一个string对象,从给定的输入流中读入内容,直到遇到换行符为止(换行符也被读入),所读内容存入string对象中。
getline返回它的流参数,因此可以作为判断条件。流的判断条件为:流有效为真,流无效(结束标记或非法输入)则为假。
#include
#include
using std::cin;
using std::cout;
using std::string;
int main() {
string s;
while(getline(cin, s))
cout << s << endl;
return 0;
}
string的empty和size操作
empty检测string对象是否为空,返回一个布尔类型的值它是string的一个成员函数,调用该函数只要使用点操作符指明是哪个对象执行了empty函数就行,以getline中string对象为例:s.empty()
即可判断s是否为空。
size函数返回string对象的长度(即string对象中字符个数),使用同empty函数相同:s.size()
。
string::size_type类型
size函数的返回类型为size_type
。其为string的配套类型,体现了标准库类型与机器无关的特性,它是在类string中定义的。
size_type
是一个无符号类型的值,能够存放任何string对象的大小。在C++11新标准中,允许编译器通过auto或者decltype来推断变量类型:auto length = s.size();
需要注意size_type
是一个无符号类型!
比较string对象
string类定义了用于比较字符串的运算符,这些运算符逐一比较string对象中的字符,且对大小写敏感。
相等性运算符==和!=
分别检验两个stirng对象相等或不想等,相等意味着它们的长度相同且包含的字符也全部相同。
<、<=、>、>=
操作按照字典排序
两个string对象相加
两个string对象相加得到一个新的string对象,使用加法运算符+
,也可使用+=
操作符将string对象的内容追加到目标string对象中。
字面值和string对象相加
可以将string对象和字面值直接相加:
string s4 = "zhc";
string s5 = s4 + "jiayou";
需要注意字面值之间不能直接相加,C++语言中的字符串字面值并不是标准库类型string的对象
处理string对象中的字符
单独处理string对象中的字符涉及到如何获取字符本身的问题以及如何对特定字符进行操作。在头文件cctype
中定义了一组标准库函数来进行这些操作:
函数 | 作用 |
---|---|
isalnum(c) | 当c是字母或数字时为真 |
isalpha(c) | 当c是字母时为真 |
iscntrl(c) | 当c是控制字符时为真 |
isdigit(c) | 当c是数字时为真 |
isgraph(c) | 当c不是空格但可打印时为真 |
islower(c) | 当c是小写字母时为真 |
isprint(c) | 当c是可打印字符时为真(即c是空格或c具有可视形式) |
ispunct(c) | 当c是标点符号时为真(即c不是控制字符、数字、字母、可打印空白中的一种) |
isspace(c) | 当c是空白时为真(即c是空格、横向制表符、纵向制表符、回车符、换行符、进纸符中的一种) |
isupper(c) | 当c是大写字母时为真 |
isxdigit(c) | 当c时十六进制数字时为真 |
tolower(c) | 如果c是大写字母,输出对应的小写字母,否则原样输出c |
toupper(c) | 如果c是小写字母,输出对应的大写字母,否则原样输出c |
使用基于范围的for语句处理每个字符
范围for语句遍历给定序列中的每个元素并对序列中的每个值执行某种操作。下面是一个例子:
string s("zhc");
for(auto c : s)
cout << c << endl;
这里通过auto关键字让编译器决定变量c的类型。这里c的类型是char,每一次迭代,s的下一个字符被拷贝给c。
如果想要改变string对象中字符的值,需要把循环变量c定义成引用类型。
这样c实际上被绑定到了序列的每个元素上,使用这个引用,我们就能修改其绑定字符的值。
string s("zhc");
for(auto &c : s)
c = toupper(c);
cout << s << endl;
这个例子的输出是ZHC
,s中的每个字符都被修改为对应的大写字母。
如果我们需要访问的只是string对象中的一个字符,或者访问多个字符但是遇到某个条件就要停下来,比如说我们指向把string对象中的第一个字母大写,有两种方式:一种是下标,另外一种是迭代器。
下标运算符
接收string::size_type
类型的参数,这个参数表示要访问的字符的位置,返回值是该位置上字符的引用。
string对象的下标从0开始计数,s[s.size() -1]
是最后一个字符。
string对象的下标必须大于等于0而小于s.size()
。使用超出此范围的下标将引发不可预知的后果,使用下标访问空string也会引发后果
下标的值被称为“下标”或“索引”,下面是一个输出string对象中第一个字符的例子:
if(!s.empty())
cout << s[0] << endl;
这里使用了逻辑非运算符!
,它返回与其运算对象相反的结果。
首先我们检查string对象是否为空,如果为空,则s[0]的结果是未定义的。现在我们修改上一小节的程序,使用下标修改字符:
string s("zhc");
if(!s.empty())
s[0] = toupper(s[0]);
程序输出Zhc
。
我们可以自己尝试自己用下标替代范围for进行迭代。
使用下标可以对string对象进行随机访问,也就是可以通过下标值访问string对象对应的字符。注意下标范围!!
标准库类型vector
vector表示对象的集合,其中所有对象的类型都相同,结合中每个对象都有对应的索引,用于访问对象。因为vector“容纳着”其他对象,所以它也被称为容器。
使用vector需要引入头文件vector
。注意using声明操作
#include
using std::vector
vector是一个类模版。类模版可以看作为编译器生成类或函数编写的一份说明,编译器根据模版创建类或函数的过程称之为实例化。
对于类模版来说,我们通过提供一些额外信息来制定模版到底实例化成什么样的类。提供信息的方式为在模版名字后面跟一对尖括号,在括号内放上信息,以vector为例:vector
ivec保存int类型的对象。
因为引用不是对象,所以不存在包含引用的vector。组成vector的元素也可以是vector:vector
在早期的C++版本中,如果vector的元素还是vector,或者其他模版类型,则其定义的形式与现在的C++11新标准有所不同,必须在外层vector对象的右尖括号和其元素之间添加一个空格,例如:vector
。在某些编译器中可能还在使用这种风格,这点需要注意。
定义和初始化vector对象
常用方法:
vector v1 //v1是一个空vector,它潜在的元素是T类型的,执行默认初始化
vector v2(v1) //v2中包含有v1所有元素的副本
vector v2 = v1 //等价于v2(v1)
vector v3(n, val) //v3包含了n个重复的元素,每个元素的值都是val
vector v4(n) //v4包含了n个重复地执行了值初始化的对象
vector v5{a,b,c...} //v5包含了初始值个数的元素,每个元素被赋予相应的初始值
vector(T) v5 = {a,b,c...} //等价于v5{a,b,c...}
列表初始化vector对象
vector
C++11新标准提供了列表初始化方法,上述表达式中vector包含三个字符串对象。
初始化方式很多且可以相互等价使用,不过要注意两种情况:1.使用拷贝初始化时(=
),只能提供一个初始值;2.如果提供的是一个类内初始值,则只能使用拷贝初始化或使用花括号形式初始化。且有一种特殊要求,如果提供的是初始元素值列表,则只能把初始值都放在花括号里进行列表初始化。
值初始化
通常情况下可以只提供vector对象容纳的元素数量而不用略去初始值,此时库会创建一个值初始化的元素初值,并把它赋给容器中的所有元素。这个初值由vector对象中元素的类型决定:
vector ivec(10) //10个元素,每个都初始化为0
vector svec(10) //10个元素,每个都是空string对象
这里有两个限制:1.有些类要求必须明确地提供初始值。2.如果只提供了元素的数量而没有设定初始值,只能使用直接初始化。
向vector对象中添加元素
对于大量的数据,运用初始化的方式便不太方便,更好的处理方法是先创建一个空vector,然后在运行时再利用vector的成员函数push_back
向其中添加元素,push_back
把一个值当成vector对象的尾元素“压到(push)“vector对象的”尾端(back)“:
vector v2; //空的vector对象
for(int i = 0; i < 100; ++i)
v2.push_back(i); //依次把整数值i放到v2的尾端
循环完成后v2对象中有100个0-99
的int
类型的元素。
如果循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环
其他vector操作
函数 | 作用 |
---|---|
v.empty() | 判断v是否为空 |
v.size() | 返回v中元素的个数 |
v.push_back(t) | 向v的尾端添加一个值为t的元素 |
v[n] | 返回v中第n个位置上元素的引用 |
v1 = v2 | 用v2中元素的拷贝替换v1中的元素 |
v1 = {a,b,c...} | 用列表中元素的拷贝替换v1中的元素 |
v1 == v2, v1 != v2 | v1和v2相等当且仅当它们的元素数量相同且对应位置的元素值都相同 |
<, <=, >, >= | 以字典顺序进行比较 |
使用范围for语句处理vector对象中的所有元素:
vector v{1,2,3,4,5,6,7,8,9};
for(auto &i : v)
i *= i;
for(auto i : v)
cout << i << " ";
cout << endl;
vector的size()
函数返回类型是由vector定义的size_type类型:
vector
不能用下标形式添加元素,用push_back!
迭代器
通用的访问string对象的字符或vector对象的元素的机制,所有标准库容器都可以使用迭代器。类似于指针类型,迭代器提供了对对象的简介访问。就迭代器而言,其对象是容器中的元素或者string对象中的字符。
使用迭代器
有迭代器的类型同时拥有返回迭代器的成员。比如这些类型都拥有名为begin和end的成员,其中begin成员返回指向第一个元素(或第一个字符)的迭代器:
auto b = v.begin(), e = v.end(); //b和e类型相同,b表示v的第一个元素,e表示v尾元素的下一位置
end成员指示的是容器的一个本不存在的“尾后”元素。end成员返回的迭代器常被称作尾后迭代器。特殊情况下如果容器为空,则begin和end返回的是同一个迭代器,都是尾后迭代器。
迭代器运算符
使用==
和!=
来比较两个合法的迭代器是否相等,如果两个迭代器指向的元素相同或者都是同一个容器的尾后迭代器,则它们相等。
操作 | 作用 |
---|---|
*iter | 返回迭代器iter所指元素的引用 |
iter->men | 解引用iter并获取该元素的名为men的成员,等价于(*iter).men |
++iter | 令iter指示容器中的下一个元素 |
--iter | 令iter指示容器中的上一个元素 |
iter1 == iter2或iter1 != iter2 | ...... |
迭代器通过解引用方式来获取它所指示元素,执行解引用的迭代器必须合法并确实指示着某个元素。
使用迭代器便利string对象,将string对象中的第一个单词改为大写形式:
string s("ahc cpp");
for(auto i = s.begin(); i != s.end() && !isspace(*i); ++i)
*i = toupper(*i);
程序输出ZHC cpp
。
迭代器类型
拥有迭代器的标准库类型使用iterator和const_iterator来表示迭代器的类型。
vector::iterator it; //it能读写vector的元素
string::iterator it2; //it2能读写string对象中的字符
vector::const_iterator it3; //it3只能读元素,不能写元素
string::const_iterator it4; //it4只能读字符,不能写字符
如果vector对象或stirng对象是一个常量,只能使用const_iterator;如果不是,则都能用。
我们认定某个类型是迭代器当且仅当它支持一套操作,这套操作使得我们能访问容器的元素或者某个元素移动到另外一个元素。
为了便于专门得到const_iterator类型的返回值,C++11新标准引入了两个新函数,分别是cbegin
和cend
。
push_back会使vector对象的迭代器失效,单反使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。
迭代器运算
string和vector的迭代器提供了许多额外的运算符,一方面可使得迭代器的每次移动跨国多个元素,另外也支持迭代器进行关系运算。所有这些运算被称作迭代器运算。
迭代器的算术运算
可以令迭代器和一个整数值相加(或相减),其返回值是向前(或向后)移动了若干个位置的迭代器:
auto mid = vi.begin() + vi.size() / 2;
mid
指向vector对象中间位置的元素。
只要两个迭代器指向的是同一个容器中的元素或者尾元素的下一位置,就能将其相减,所得结果是两个迭代器的距离,其类型是名为difference_type的带符号整型数。
数组
数组是一种类似于标准库类型vector的数据机构。与vector相似的地方是,数组也是存放类型相同的对象的容器。与vector不同的地方时,数组的大小确定不变,不能随意向数组中增加元素。如果不清楚元素的确切个数,请使用vector。
定义和初始化内置数组
数组是一种复合类型。数组的声明形如a[d]
,其中a是数组的名字,d是数组的维度。维度说明了数组中元素的个数,因此必须大于0。维度必须是一个常量表达式。
unsigned cnt = 42; //不是常量表达式
constexpr unsigned sz = 42; //常量表达式
int arr[10]; //含有10个整数的数组
int *parr[sz]; //42个整型指针的数组
string bad[cnt]; //错误:cnt不是常量表达式
string strs[get_size()]; //当get_size是constexpr时正确,否则错误
默认情况下,数组的元素被默认初始化。
数组的元素应是对象,因此不存在引用的数组。不允许用auto关键字推断数组类型。
显式初始化数组元素
可以对数组元素进行列表初始化,此时允许忽略数组的维度。编译器会根据初始值的数量计算并推测出来。
字符数组的特殊性
字符数组有一种额外的初始化形式,用字符串字面值对数组进行初始化。注意字符串字面值的结尾还有一个空字符。
const char a4[6] = "Daniel"; //错误:没有空间可存放空字符串
不能将数组的内容拷贝给其他数组作为初始值,也不能用数组为其他数组复制。
数组的指针或引用
int *ptrs[10]; //ptrs是含有10个整型指针的数组
int &refs[10] = ... //错误:不存在引用的数组
int (*Parray)[10] = &arr; //Parray指向一个含有10个整数的数组
int (&arrRef)[10] = arr; //arrRef引用一个含有10个整数的数组
访问数组元素
数组的元素能够使用范围for语句或下标运算符来访问。
在使用数组下标的时候,通常将其定义为size_t类型。这是一种机器相关的无符号类型,它足够大,能表示内存中任意对象的大小,其头文件为cstddef
。
与vector和stinrg一样,数组的下标是否在合理范围之内由程序员负责检查,就说指下标应该大于等于0且小与数组的大小。
指针和数组
使用数组的时候编译器一般会把它转换成指针。对数组的元素使用取地址符就能得到指向该元素的指针:
string nums[] = {"one", "two", "three"}; //数组的元素是string对象
string *p = &nums[0]; //p指向nums的第一个元素
在很多用到数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指针:string *p2 = nums; //等价于p2 = &nums[0]
。
在一些情况下数组的操作实际上是指针的操作,所以当使用数组作为一个auto变量的初始值时,推断得到的类型是指针而非数组。
当使用decltype关键字时,数组返回的类型是由10个整数构成的数组:
decltype(某数组) ia = {0,1,2,3,4,5,6,7,8,9};
//这个时候ia是一个数组
指针也是迭代器
vector和string的迭代器支持的运算,数组的指针全部支持。
有一个问题是如何获取指向数组尾后指针,这需要用到数组的一个特殊性质,我们可以设法获取数组尾元素之后的那个并不存在的地址,这个时候也就获取到了数组之后一个并不存在的元素,也就相当于尾后迭代器一样。
但是这样做并不安全。C++11新标准引入了两个名为begin
和end
的函数,这里的函数不像是vector里的同名函数那样是vector标准库的成员函数:
int ia[] = {0,1,2,3,4,5,6,7,8,9};;
int *beg = begin(ia); //指向ia首元素的指针
int *last = end(ia); //指向ia尾元素的下一位置的指针
begin
和end
定义在头文件iterator
中。
尾后指针不能进行解引用和递增操作!
指针运算的操作与迭代器类似,可翻看前面几节迭代器的操作。
数组中两个指针相减的结果是一种名为ptrdiff_t
的标准库类型,和size_t
一样定义在头文件cstddef
中,与机器相关,是一种带符号的类型。
C风格字符串
字符串字面值是一种通用结构的实例,是C++由C即成而来的C风格字符串。按此结构书写的字符串存放在字符数组中并以空字符'\0'
结束。
对大多数应用来说,使用标准库string要比使用C风格字符串更安全、更高效。
c_str
函数将string对象转换为C风格字符串,返回值是一个C风格的字符串,是一个指针,指向一个以空字符结束的字符数组。指针的类型是const char*
。
如果执行完c_str()
函数后程序想一直都能使用其返回的数组,最好将该数组重新拷贝一份。
允许使用数组初始化vector:
int int_arr[] = {0,1,2,3,4,5};
vector ivec(begin(int_arr), end(int_arr));
创建ivec的两个指针指明了用来初始化的值在int_arr中的为孩子,其中第二个指针应指向待拷贝区域尾元素的下一位置。
现代的C++程序应当尽量使用vector和迭代器,避免使用内置数组和指针;应该尽量使用string,避免使用C风格的基于数组的字符串。
多维数组
严格来说,C++语言中没有多维数组,通常所说的多维数组其实是数组的数组。
int ia[3][4]; //二维数组
int arr[10][20][30]; //三维数组,且数组哪元素都被初始化为0
对于二维数组来说,常把第一个维度称为行,第二个维度称为列。
对于多维数组的初始化和操作可以参考网上或书本资料,《C++ primer》上有些繁杂,在此就不加入笔记了。