第三章 字符串、向量和数组
3.2 标准库类型string
3.3.2 string对象上的操作
string的操作 | |
---|---|
os<<s | 将s写到输出流os中,返回os |
is>>s | 从is中读取字符串赋值给s,字符串以空白分隔,返回is |
<,<=,>,>= | 利用字符在字典中的顺序进行比较,string对象对字母的大小写敏感 |
getline(is,s) | 从is中读取一行复制给s,返回is |
使用getline读取一整行
若希望得到的字符串中保留输入时的空白符,使用getline函数代替>>运算符。getline函数从给定的输入流中读入内容,直到遇到换行符为止(注意换行符也被读进来了),然后把所读内容赋值给string对象(注意不存换行符)。
string::size_type类型
对于size函数,返回一个int或者unsigned似乎是合情合理,但其实size函数返回的是string::size_type类型的值。size_type是一个无符号类型的值而且能足够存放任何string对象的大小,所有用来存放string类的size函数返回值的变量,都应该是该类型。!!!注:若一条表达式中已经有了size函数就不要使用int来接受size函数返回的变量,这样可以避免混用int和unsigned带来的问题!!!如:n是一个具有负值的int,则表达式s.size()<n判断结果几乎肯定是true,因为负数n会自动转换成一个比较大的无符号值。
字面值和string对象相加
注:当把string对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符的两侧的运算对象至少一个是string:
string s6=s1+","+"world";//正确:每个加法运算符都有一个运算对象是string
string s7="hello"+","+s2;//错误:不能把字面值直接相加
!!!注:为了与C兼容,C++语言中的字符串字面值并不是标准库类型string的对象。
3.2.3 处理string对象中的字符
如下标准库函数可以对string对象中的字符进行处理:
函数 | 含义 |
---|---|
isalnum© | 当c是字母或数字时为真 |
isalpha© | 当c是字母时为真 |
isdigit© | 当c是十进制数字时为真 |
islower© | 当c是小写字母时为真 |
isupper© | 当c是大写字母时为真 |
isxdigit© | 当c是十六进制数字时为真 |
tolower© | 若c是大写字母,输入对应的小写字母;否则原样输出 |
toupper© | 若c是小写字母,输入对应的大写字母;否则原样输出 |
isspace© | 当c为标点符号时为真(即c不是控制字符、数字、字母、可打印空白中的一种) |
处理每个字符?使用基于范围的for语句
C++11新标准提出了一种基于范围的for语句,该语句遍历给定序列中的每个元素并对序列中的每个值的执行某种操作,其语法形式是:
for(declaration:expression){
statement
}
其中expression部分是一个对象,用于表示一个序列;declaration部分负责定义一个变量,该变量被用于访问序列中的基本元素;每次迭代,declaration部分的变量都会被初始化为expression部分的下一元素值。例:string对象表示一个字符的序列,可以作为expression部分,下面代码段把string对象str中的字符一次打印出来。
string str("some thing");
for(auto c:str){ //关键词auto判断c的类型 为char
cout<<c<<endl;
}
使用范围for语句改变字符串中的字符
若向改变string对象中字符的值,必须把循环变量定义成引用类型。所谓引用就是给定对象的一个别名,引用绑定在给定对象上,使用引用作为变量时,对引用的操作就是对给定对象的操作,进而可以改变给定对象的值。
for(auto &c:str){
c=toupper(c);//c是引用,赋值将改变s中字符的值
}
cout<<str<<endl;//str输出为“SOME THING”
注:for语句的条件部分涉及一点新知识,逻辑与(&&)。C++语言规定只有当左侧运算对象为真时才会检查右侧运算对象的情况。
3.3 标准库类型vector
模板本身不是类或函数,相反可以将模板看作为编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程称为实例化,当使用模板时,需要指出编译器应把类或函数实例化成何种类型。
vector是一个类模板,对于类模板我们需要提供一些额外消息来指定模板到底实例化成什么样的类,需要提供哪些消息由模板决定。**方式为:在模板名字后面跟一对尖括号,在括号内放上信息。**如:
vector<int> ivec;//ivec保存int类型对象
vector<Sales_item> Sales_vec;//Sales_vec保存Sales_item类型对象
vector<vector<string>>file;//file保存vector对象
注:vector虽能容纳绝大数类型的对象作为元素,但是不可以包含引用,因为引用不是对象。
3.4 迭代器介绍
我们直到可以通过下标运算符来访问string对象的字符或vector对象的元素,还有另外一种更通用的机制可以实现同样的目的,就是迭代器(iterator)。标准库下定义的几种容器都可以使用迭代器,但是其中只有几种才同时支持下标运算符。!!!注:严格来说,string对象不属于容器类型,但是string支持与容器类型类似的操作。
3.4.1 使用迭代器类型
迭代器类型
拥有迭代器的标准库类型使用iterator和const_iteratorl来表示迭代器的类型。iterator类型能读写元素,而const_iteratorl仅能读元素,不可写元素。如:
vector<int>::iterator it;//it能读写vector<int>的元素
vector<int>::const_iterator it2;//it2仅能读vector<int>的元素,不能写元素
const_iteratorl和常量指针类似,能读取但不能修改它所指的元素值。若vector对象是一个常量,只能使用const_iteratorl;若不是常量,既可以使用iterator又可以使用const_iteratorl。
begin和end运算符
begin和end返回的具体类型由对象是否是常量决定。若是常量,返回const_iteratorl;若不是常量,返回iterator。
vector<int> v;
const vector<int> cv;
auto it1=v.begin();//it1类型是vector<int>::iterator
auto it2=v.end();//it2类型是vector<int>::const_iterator
C++11新标准引入了两个新函数,cbegin和cend函数,与begin和end函数不同的是,不论对象是否是常量返回的都是const_iteratorl。
auto it3=v.cbegin();//it3类型是vector<int>::const_iterator
结合解引用和成员访问操作
解引用迭代器可获得迭代器所指的对象。例:一个由字符串组成的vector对象来说,要想检查元素是否为空,令it是该vector对象的迭代器,只需检查it所指字符串是否为空就可以了,其代码为:
(*it).empty();
it->empty();//两者表达意思一样
某些对vector对象的操作会使迭代器失效
虽然vector对象可以动态地增长,但是也会有副作用。第一个限制是不能在范围for循环中向vector容器添加元素;第二个限制是任何一种可能改变vector对象容量的操作,比如push_back,都会使该vector对象的迭代器失效。!!!注:但凡使用了迭代器的循环体,都不可以向迭代器所属的容器中添加元素。
3.5 数组
3.5.1 定义和初始化内置数组
数组是一种复合类型,声明形如:a[d],其中a是数组名,d是数组的维度,即数组中元素的个数,必须大于0且是一个常量表达式。
注:和内置类型的变量一样,若在函数内部定义了某种内置类型的数组,那么默认初始化会令数组含有未定义的值;定义数组时必须指定数组的类型,不允许使用auto关键字由初始值的列表推断类型;和vector一样,数组的元素应为对象,因此不存在引用的数组。
字符数组的特殊性
字符数组可以用字符串字面值进行初始化,但使用这种方式时 ,注意字符串字面值的结尾还有一个空字符,这个空字符也会被拷贝到字符数组中。
char a3[]="C++";//自动添加表示字符串结束的空字符
const char a4[6]="abcdef"//错误:没有空间存放空字符
理解复杂的数组声明
数组能存放大多数类型的对象,如定义一个存放指针的数组。又因为数组本身就是对象,所以允许定义数组的指针及数组的引用。
int *ptrs[10];//ptrs是含有10个整型指针的数组
int &refs[10];//错误:不存在引用的数组
int (*Parray)[10]=&arr;//Parray指向一个含有10个整数的数组
int (&arraRef)[10]=arr;//arrRef引用一个含有10个整数的数组
默认情况下,类型修饰符从右向左依次绑定,如ptrs。就数组而言,由数组名内部向外阅读要比从右向左好多了,如*Parray是一个指针,观察右边,直到Parry是个指向大小为10的数组的指针,最后观察左边,知道数组类型为int,所以Parray是一个指针,它指向一个int数组,数组中包含10个元素。
对修饰符的数量也没有特殊限制:
int *(&arry)[10]=ptrs;//arry是数组的引用,该数组有10个指针
由上述由内往外原则,arry是一个引用,观察右边,引用的对象是一个大小为10的数组,在观察左边,数组的元素类型是指向int的指针,所有arry就是一个含有10个int型指针的数组的引用。
3.5.2 访问数组元素
可以使用范围for或者下标运算符来访问。
在使用数组下标时,通常将其定义为size_t类型。size_t是一种机器相关的无符号类型,它被设计得足够大以便能表示内存中任意对象的大小,在cstddef头文件中定义,继承与C语言stddef.h头文件。
检查下标的值
防止数组下标越界。!!!注:大多数常见的安全问题都源于缓冲区溢出错误。当数组或者其他类似数据结构的下标越界并试图访问非法内存区域时,就会产生此类错误。
3.5.3 指针和数组
在C++语言中,指针和数组有非常密切的联系。使用数组的时候编译器一般会把它转换成指针。
注:在大多数表达式中,使用数组类型的对象其实是使用一个指向该数组首元素的指针。
string nums[]={"one","two"};
string *p=nums[0];//p指向nums的第一个元素
string *p2=nums;//等价于p2=&nums[0];
使用数组作为一个auto变量的初始值时,推断得到的类型是指针而非数组:
auto n(nums);//n是一个string类型指针,指向nums第一个元素
//编译器实际执行:
auto n(&nums[0]);//显然n类型string *
n="three";//错误:n是一个指针,不能用string值给指针赋值
当使用decltype关键字时上述转换不会发生:
//n2时一个含有2个string的数组
decltype(nums) n2={"one","two"};
n2=p;//错误:不能用指针给数组赋值
n2[1]="three";//正确:把string值赋给n2的一个元素
标准库函数begin和end
因为数组不是类类型,因此这两个函数不是成员函数。正确的使用形式是将数组作为它们的参数:
int a[]={1,2,3,4,5};
int *begin=begin(a);//指向a首元素的指针
int *end=end(a);//指向a尾元素的下一位置的指针
!!!注意:尾后指针不能执行解引用和递增操作。
指针运算
数组中两个指针相减的结果是它们之间的距离,类型是ptrdiff_t的标准库类型,和size_t一样定义在cstddef头文件中机器相关的类型,因为差值可能为负,所有pridiff_t是一种带符号类型。
3.5.4 C风格字符串
字符串字面值是C++由C继承而来的C风格字符串 。C风格字符串不是一种类型,而是为了表达和使用字符串而形成的一种约定俗成的写法,按此习惯书写的字符串存放在字符数组中并比空字符串结束(‘\0’);一般使用指针操作这些字符串。
C标准库String函数
C语言标准库提供了一组函数,这些函数用于操作C风格字符串,它们定义在cstring头文件中,cstring是C语言头文件string.h的C++版本。
函数 | |
---|---|
strlen§ | 返回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 |
!!!注:传入此类函数的指针必须指向以空字符作为结束的数组:
char ca[]={'c','+','+'};//ca数组不以空字符结束
cout<<strlen(ca)<<endl;//严重错误:ca没有以空字符结束
因为ca数组没有以空字符结束,因此上述程序将会产生未定义的结果。strlen函数将有可能沿着ca在内存中的位置不断向前寻找,直到遇到空字符才停下来。
比较字符串
C风格字符串和C++标准库string对象进行比较的方法大相径庭。比较标准库srting对象用的是普通的关系运算符和相等性运算符;C风格字符串比较的将是指针而非字符串本身,要想比较两个C风格字符串需要调用strcmp函数。
3.5.5 与旧代码的接口
混用string对象和C风格字符串
3.2.1节介绍过允许使用字符串字面值来初始化string对象:
string s("Hello World");//s的内容是Hello World
更一般的情况是,任何出现字符串字面值的地方都可以用以空字符结束的字符数组来替代:
情况 |
---|
允许使用以空字符结束的字符数组来初始化string对象或为string对象赋值 |
在string对象的加法运算中允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个运算对象都是);在string对象的复合赋值运算中允许使用以空字符结束的字符数组作为右侧的运算对象。 |
注:上述性质反过来就不成立了:若程序的某处需要一个C风格字符串,无法使用string对象来替代它。例如,不能用string对象直接初始化指向字符的指针。为了实现string对象直接初始化C风格字符串,string专门提供了一个名为c_str的成员函数,c_str函数的返回值是一个C风格的字符串,即一个指针,该指针返回一个以空字符结束的字符数组,这个数组所存的数据恰好与那个string对象一样。
char *str=s;//错误:不能用string对象初始化char*
const char *str=s.c_str();//正确
使用数组初始化vector对象
前文提到过不允许使用一个数组为另一个内置类型赋初值,也不允许使用vector对象初始化数组。相反,允许使用数组来初始化vector对象,要实现这一目的,只需要指明要拷贝区域的首元素地址和尾后地址就可以了:
int int_arr[]={0,1,2,3,4,5};
vector<int> ivec(begin(int_arr),end(int_arr));//ivec有6个元素,次序和值都与数组int_arr完全一样。
3.6 多维数组
严格来说,C++语言中没有多维数组,通常所说的多维数组其实是数组的数组。
多维数组的下标引用
#include<iostream>
using namespace std;
//使用两层嵌套的for循环来处理多维数组的元素
int main(){
constexpr size_t rosCnt=3,colCnt=4;
int ia[rosCnt][colCnt]={
{1,2,3,4},
{5,6,7,8},
{9,10,11,12}
};
for(size_t i=0;i!=rosCnt;++i){
for(size_t j=0;j!=colCnt;++j){
ia[i][j]=i*colCnt+j;
cout<<ia[i][j]<<" ";
}
cout<<endl;
}
}
使用范围for语句处理多维数组
#include<iostream>
using namespace std;
//使用范围for语句处理多维数组
int main(){
constexpr size_t rosCnt=3,colCnt=4;
size_t cnt=0;
int ia[rosCnt][colCnt]={
{1,2,3,4},
{5,6,7,8},
{9,10,11,12}
};
//因为要改变元素的值,所以把控制变量row和col声明成引用类型
for(auto &row:ia){
for(auto &col:row){
col=cnt;
++cnt;
cout<<col<<" ";
}
cout<<endl;
}
}
第一个for循环遍历ia的所有元素,row的类型是含有4个整数的数组的引用;第二个for循环遍历那些4元素数组中的某一个,因此col的类型是整数的引用。
在上面的例子中,因为要改变数组元素的值,所以我们选用引用类型作为循环控制变量,但其实还有一个深层次的原因促使我们这么做。例如:
for(const auto &row:ia){//对于外层数组的每一个元素
for(auto col:row){//对于内层数组的的每一个元素
cout<<col<<" ";
}
}
该循环中并没有任何写操作,可是还是将外层循环的控制变量声明成了引用类型,这是为了避免数组被自动转换成指针。假如不用引用类型,则循环如下述形式:
for(auto row:ia){ //row类型是int*
for(auto col:row)//内层循环不合法
}
程序将无法通过编译。这是因为第一个for循环遍历ia的所有元素,注意这些元素实际上是大小为4的数组。因为row不是引用类型,所以编译器初始化row时会自动将这些数组形式的元素(和其他类型的数组一样)转换成指向该数组内首元素的指针。这样得到的row的类型就是int *,显然内层的循环就不合法了。
!!!注:要使用范围for语句处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应使用引用类型。
指针和多维数组
因为多维数组实际上是数组的数组,所以由多维数组名转换得来的指针实际上是指向第一内层数组的指针:
int ia[3][4];
int (*p)[4]=ia;//p指向含有4个整数的数组
p=&ia[2];
注:在上述声明中,圆括号必不可少:
int *ip[4];//整数指针的数组
int (*p)[4]=ia;//p指向含有4个整数的数组