C++Primer学习笔记_第三章 字符串、向量和数组

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

本章将介绍两种最重要的标准库类型:string和vector。string表示可变长的字符序列,vector存放的是某种给定类型对象的可变长序列。本章还将介绍内置数组类型。


3.1 命名空间的using声明

我们用到的库函数基本上都属于命名空间std。

通过更简单的途径能使用到命名空间中的成员,如使用using声明(using declaration)

有了using声明就无须专门的前缀(形如命名空间::)也能使用所需的名字了。using声明具有如下形式:

using namespace::name;

一旦声明了上述语句,就可以直接访问命名空间中的名字:

#include<iostream>
//using声明,当我们使用名字cin时,从命名空间std中获取它
using std::sin;
int main()
{
	int i;
	cin>>i;			//正确:cin和std::cin含义相同
	cout<<i;		//错误:没有对应的using声明,必须使用完整的名字
	std::cout<<i;	//正确:显式地从std中使用cout
	return 0;
}
每个名字都需要独立的using声明

按照规定,每个using声明引入命名空间中的一个成员。

头文件不应包含using声明

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


3.2 标准库类型string

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

using std::string;

3.2.1 定义和初始化string对象

初始化string对象最常用的一些方式:

string s1;			//默认初始化,s1是一个空字符串
string s2=s1;		//s2是s1的副本
string s3="hiya";	//s3是该字符串字面值的副本
string s4(10,'c');	//s4的内容是cccccccccc(10个c)

初始化string对象的方式

直接初始化和拷贝初始化

如果使用等号(=)初始化一个变量,实际上执行的是拷贝初始化(copy initialization)。与之相反,如果不使用等号,则执行的是直接初始化(direct initialization)

当初始值只有一个时,使用直接初始化或拷贝初始化都行。如果像s4那样初始化要用到的值有多个,一般来说只能使用直接初始化的方式。

3.2.2 string对象上的操作

一个类除了要规定初始化其对象的方式外,还要定义对象上所能执行的操作。
string的操作

读写string对象

可以使用IO操作符读写string对象。

在执行读取操作时,string对象会自动忽略开头的空白(即空格符、换行符、制表符等)并从第一个真正的字符开始读起,直到遇见下一处空白为止。如果程序只定义一个string对象,而输入的是“ Hello world! ”,则输出将是“Hello”,输出结果没有任何空格。如果定义两个string对象,并且连续输入输出,则输出则是“HelloWorld”。

读取未知数量的string对象
int main()
{
	string word;
	while(cin>>word)
		cout<<word<<endl;
		return 0;
}
使用getline读取一整行

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

getline只要一遇到换行符就结束读取操作并返回结果,哪怕输入的一开始就是换行符也是如此。

string的empty和size操作

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

empty函数是string的一个成员函数。

//只输出非空的行
while(getline(cin,line))
if(!line.empty())//逻辑非运算符(!)
	cout<<line<<endl;

size函数返回string对象的长度

string::size_type类型

size函数返回的是一个string::size_type类型的值。

string类及其他大多数标准库类型都定义了几种配套的类型,如size_type

所有用于存放string类的size函数返回值的变量,都应该是string::size_type类型的。

假设n是一个具有负值的int,则表达式s.size()<n的判断结果几乎肯定是true,这是因为负值n会自动地转换成一个比较大的无符号值。

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

比较string对象

string对象的比较依照(大小写敏感)字典顺序:
1.如果两个string对象的长度不同,而且较短string对象的每个字符都与较长string对象对应位置上的字符相同,就说较短string对象小于较长string对象。
2.如果两个string对象再某些对应的位置上不一致,则string对象比较的结果其实是string对象中第一对相异字符比较的结果。

string str="Hello";
string phrase="Hello World";
string slang="Hiya";

根据规则1可判断,对象str小于对象phrase;根据规则2可判断,对象slang既大于str也大于phrase。

为string对象赋值

对于string类而言,允许把一个对象的值赋给另外一个对象。

两个string对象相加

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

字面值和string对象相加

因为标准库允许把字符字面值和字符串字面值转换成string对象,所以在需要string对象的地方就可以使用这两种字面值来替代。

string s1="hello",s2="world";
string s4=s1+","+s2+'\n';

当把string对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符(+)的两侧的运算对象至少有一个是string:

string s4=s1+",";			//正确
string s5="hello"+",";		//错误,两个运算对象都不是string
string s6=s1+","+"world";	//正确,等于string s6=(s1+",")+"world"
string s7="hello"+","+s2;	//错误,等于string s7=(hello+",")+s2,不能把字面值直接相加

注:字符串字面值与string是不同的类型

3.2.3 处理string对象中的字符

cctype头文件中的函数

建议:使用C++版本的C标准库头文件
如果使用.h形式的头文件,程序员就不得不时刻牢记哪些是从C语言那儿继承过来的,哪些优势C++语言所独有的

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

如果想对string对象中的每个字符做点什么操作,目前最好的办法是使用C++11新标准提供的一种语句:**范围for(range for)**语句。这种语句遍历给定序列中的每个元素并对序列中的每个值执行某种操作,其语法形式是:

for(declaration:expression)
statement

其中,expression部分是一个对象,用于表示一个序列。declaration部分负责定义一个变量,该变量将被用于访问序列中的基础元素。每次迭代,declaration部分的变量会被初始化expression部分的下一个元素值。

我们可以使用范围for语句把string对象中的字符每行一个输出出来:

string str("some string");
//每行输出str中的一个字符
for(auto c:str)		//对于str中的每个字符
	cout<<c<<endl;	//输出当前字符,后面紧跟一个换行符

我们也可以使用范围for语句和ispunct函数来统计string对象中标点符号的个数:

stirng s("Hello World!!!");
//punct_cnt的类型和s.size的返回类型一样
decltype(s.size()) punct_cnt=0;
//统计s中标点符号的数量
for(auto c:s)
	if(ispunct(c))
		++punct_cnt;
cout<<punct_cnt;
	<<" punctuation characters in "<<s<<endl;
使用范围for语句改变字符串中的字符

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

//把字符串改写为大写字母的形式
string s("Hello world!!!")
for(auto &c:s)
	c=toupper(c);
cout<<s<<endl;
只处理一部分字符?

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

下标运算符[ ])接收的输入参数是string::size_type类型的值,这个参数表示要访问的字符的位置,返回值是该位置上字符的引用。

string对象的下标从0开始,s[s.size()-1]是最后一个字符。

下标的值称为“下标”或者“索引”,任何表达式只要它的值是一个整型值就能作为索引。不过,如果某个索引是带符号类型的值将自动转换为string::size_type表达的无符号类型。

只要字符串不是常量,就能为下标运算符返回的字符赋新值。

使用下标进行迭代
//把s的第一个词改成大写形式
//依次处理s中的字符直至我们处理完全部字符或者遇到一个空白
for(decltype(s.size())) index=0;
	index!=s.size()&&!isspace(s[index]);++index)
		s[index]=toupper(s[index]);
使用下标执行随机访问
//把0到15之间的十进制数转换成对应的十六进制形式
const string hexdigits="0123456789ABCDEF";//可能的十六进制数字
cout<<"Enter a series of numbers between 0 and 15";
	<<" separated by spaces. Hit ENTER when finished:"
	<<endl;
string result;
string::size_type n;				//用于保存从输入流读取的数
while(cin>>n)
	if(n<hexdigits.size())			//忽略无效输入
			rusult+=hexdigits[n];	//得到对应十六进制数字
cout<<"Your hex number is: "<<result<<endl;

3.3 标准库类型vector

标准库类型Vector表示对象的集合,其中所有对象的类型都相同。因为Vector容纳着其他对象,所以它也常被称作容器(container)

C++语言既有类模板(class template),也有函数模板,其中vector是一个类模板。编译器根据模板创建类或函数的过程称为实例化(instantiation),当使用模板时,需要指出编译器把类或函数实例化成何种类型。

vector<int> ivec;				//ivec保存int类型的对象
vector<Sales_item> Sales_vec;	//保存Sales_item类型的对象
vector<vector<string>> file;	//该向量的元素是vector对象

注:vector是模板而非类型,由vector生成的类型必须包含vector中元素的类型,例如vector< int >。

不保存包含引用的vector。

3.3.1 定义和初始化vector对象

和任何一种类类型一样,vector模板控制着定义和初始化向量的方法。
初始化vector对象的方法
可以默认初始化vector对象,从而创建一个指定类型的空vector:

vector<string> svec;//默认初始化,svec不含任何元素

往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
列表初始化vector对象

C++11新标准还提供了另外一种为vector对象的元素赋初值的方法,即列表初始化。此时,用花括号括起来的0个或多个初始元素值被赋给vector对象:

vector<string> articles={"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对象中元素的类型决定。

vector<int> ivec(10);		//10个元素,每个都初始化为0
vector<string> svec(10);	//10个元素,每个都是空string对象

对这种初始化的方法由两个特殊限制:其一,有些类要求必须明确地提供初始值,如果vector对象中元素地类型不支持默认初始化,我们就必须提供初始地元素值。其二,如果只提供了元素地数量而没有设定初始值,只能使用直接初始化。

vector<int> vi=10;//错误:必须使用直接初始化的形式指定向量大小
列表初始值还是元素数量?
vector<int> v1(10);		//v1有10个元素,每个的值都是0
vector<int> v2{10};		//v2有1个元素,该元素的值是10
vector<int> v3(10,1);	//v3有10个元素,每个的值是1
vector<int> v4{10,1};	//v4有2个元素,值分别是10和1

如果用的是圆括号,可以说提供的值是用来构造(construct)vector对象的。如果用的是花括号,可以表述成我们像列表初始化(list initialize)该vector对象。

3.3.2 向vector对象中添加元素

对vector对象来说,直接初始化的方式适用于三种情况:初始值已知且数量很少,初始值是另一个vector对象的副本、所有元素的初始值都一样。

然而更常见的情况是:创建一个vector对象时并不清楚实际所需的元素个数,元素的值也无法确定。还有即使元素的初值已知,但如果这些值总量较大而各不相同,那么在创建vector对象的时候执行初始化操作也会显得过于烦琐。

这时候经常要利用push_back函数向其中添加元素。push_back负责把一个值当成vector对象的尾元素“压倒(push)”vector对象的“尾端(back)”。

关键概念:vector对象能高效增长
C++标准要求vector应该能在运行时高效快速地添加元素。除非所有元素地值都一样,否则更有效的办法是先定义一个空的vector对象,再在运行时向其中添加具体值。

向vector对象添加元素蕴含的编程假定

必须要确保缩写的循环正确无误,特别是在循环有可能改变vector对象容量的时候。

如果循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环。
注:范围for语句体内不应改变其所遍历序列的大小。

3.3.3 其他vector操作

vector支持的操作
访问vector对象中元素的方法和访问string对象中字符的方法差不多,也是通过元素在vector对象中的位置。例如,可以使用范围for语句处理vector对象中的所有元素:

vector<int> v{1,2,3,4,5,6,7,8,9};
for(auto &i : v)	//对于v中的每个元素(注意,i是一个引用)
	i *=i;			//求元素值的平方
for(auto i : v)		//对于v中的每个元素
	cout<<i<<" ";	//输出该元素
cout<<endl;

注:要使用size_type,需首先指定它是由哪种类型定义的。vector对象的类型总是包含着元素的类型:如vector::size_type是正确的,而vector::size_type是错误的。

各个相等性运算符和关系运算符也与string的相应运算符功能一致。

只有当元素的值可比较时,vector对象才能被比较。

计算vector内对象的索引

使用下标运算符能获取到指定的元素。

不能用下标形式添加元素
vector<int> ivec;//空vector对象
for(decltype(ivec.size()) ix=0;ix != 10;++ix)
{
	ivec[ix]=ix;//严重错误:ivec不包含任何元素
}

注:vector对象(以及string对象)的下标运算符可用于访问已存在的元素,而不能用于添加元素。

提示:只能对确知已存在的元素执行下标操作!
vecotr<int> ivec;
cout<<ivec[0]//错误
vector<int>ivec2(10);
cout<<ivec[10];//错误
试图用下标的形式去访问一个不存在的元素将引发错误,不过这种错误不会被编译器发现,而是在运行时产生一个不可预知的值。这种错误会产生很严重的后果如缓冲区溢出(buffer overflow)
确保下标合法的一种有效手段就是尽可能使用范围for语句。


3.4 迭代器介绍

我们已经直到可以使用下标运算符来访问string对象的字符或vector对象的元素,还有另外一种更通用的机制也可以实现同样的目的,这就是迭代器(iterator)。所有标准库容器都可以使用迭代器。

类似于指针类型,迭代器也提供了对对象的间接访问。

3.4.1 使用迭代器

获取迭代器不是使用取地址符,有迭代器的类型同时拥有返回迭代器的成员。

比如,这些类型都拥有名为beginend成员,其中begin成员负责返回指向第一个元素的迭代器,end成员则负责返回指向容器“尾元素的下一位置(one past the end)”,也就是说,该迭代器指示的是容器的一个本不存在的“尾后(off the end)”元素。end成员返回的迭代器常被称为尾后迭代器(off-the-end iterator) 或者 尾迭代器(end iterator)

如果容器为空,则begin和end返回的是同一个迭代器。,即尾后迭代器。

一般来说,我们不在意迭代器准确的类型到底是什么,因此我们使用auto关键字定义变量来存放迭代器。

迭代器运算符

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

string s("some string");
if(s.begin()!=s.end())
{
	auto it=s.begin();
	*it =toupper(*it);
}	

输出结果:
Some string

将迭代器从一个元素移动到另外一个元素

迭代器使用递增运算符来从一个元素移动到下一个元素,使用递减运算符来从一个元素移动到上一个元素。

关键概念:泛型编程
所有标准库容器的迭代器都定义了==和!=,但是它们中的大多数都没有定义<运算符,因此,只要我们养成使用迭代器和!=的习惯,就不用太在意用的是哪种容器类型。

迭代器类型

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

vector<int>::iterator it;		//it能读写vector<int>的元素
string::iterator it2;			//it2能读写string对象中的字符
vector<int>::const_iterator it3;//it3只能读元素,不能写元素
string::const_iterator it4;		//it4只能读字符,不能写字符

术语:迭代器和迭代器类型
迭代器这个名词有三种不同的含义:可能是迭代器概念本省,也可能是指容器定义的迭代器类型,还可能是指某个迭代器对象。
重点是理解在一组概念上相关的类型,我们认定某个类型是迭代器当且仅当它支持一套操作,这套操作使得我们能访问容器的元素或者从某个元素移动到另外一个元素。
每个容器类定义了一个名为iterator的类型,该类型支持迭代器概念所规定的一套操作。

begin和end运算符

begin和end返回的具体类型由对象是否是常量决定,如果是常量,返回const_iterator,反之返回iterator。

为了便于专门得到const_iterator类型的返回值,C++11新标准引入了两个新函数,分别是cbegin和cend:

auto it3=v.cbegin();//it3的类型是vector<int>::const_iterator
结合解引用和成员访问操作

解引用迭代器可获得迭代器所指的对象,如果该对象的类型恰好是类,就有可能希望进一步访问它的成员。

(*it).empty();//解引用it,然后调用结果对象的empty成员
*it.empty();//错误:试图访问it的名为empty成员,但it是个迭代器,没有empty成员。

为了简化上述表达式,C++语言定义了箭头运算符->)。箭头运算符把解引用和成员访问两个操作结合在一起,也就是说it->men和(*it).men的意思相同。
注:谨记,但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。

3.4.2 迭代器运算

所有的标准库容器都有支持递增运算的迭代器,类似的,也能用==和!=对任意标准库类型的两个有效迭代器进行比较。所有这些运算符被称为迭代器运算(iterator arithmetic)

vector和string迭代器支持的运算

迭代器的算术运算

可以令迭代器和一个整数值相加(或相减),其返回值是向前(或向后)移动了若干个位置的迭代器。

只要两个迭代器指向的是同一个容器中的元素或者尾元素的下一个位置,就能将其相减,所得结果是两个迭代器的距离。所谓距离指的是右侧的迭代器向前移动多少位置就能追上左侧的迭代器,其类型是名为difference_type的带符号整型数。

使用迭代器运算

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

//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+1;			//在mid之后寻找
	mid=beg+(end-beg)/2;	//新的中间点
}

3.5 数组

数组是一种类似于标准库类型vector的数据结构,与vector相似的地方是,数组也是存放类型相同的对象的容器,这些对象本身没有名字,需要通过其所在位置访问,不同的是,数组的大小确定不变,不能随意向数组中增加元素。
注:如果不清楚元素的确切个数,请使用vector。

3.5.1 定义和初始化内置数组

数组是一种复合类型。数组的声明形如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 unsigned sz = 3;
int ial[sz] = {0,1,2};		//含有3个元素的数组,元素值分别是0,1,2
int a2[]={0,1,2};			//维度是3的数组
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','+','+','\0'};	//列表初始化,含有显式的空字符
char a3[]="C++";				//自动添加表示字符串结束的空字符
const char a4[6]="Daniel";		//错误:没有空间可存放空字符
不允许拷贝和赋值

不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值。
注:一些编译器支持数组的赋值,这就是所谓的编译器拓展(compiler extension)。但一般来说,最好避免使用非标准特性,因为含有非标准特性的程序很可能在其他编译器上无法正常运行。

理解复杂的数组声明
int *ptrs[10];			//ptrs是含有10个整型指针的数组
int &refs[10]=/*?*/;	//错误:不存在引用的数组
int (*Parray)[10]=&arr;	//Parray指向一个含有一个10个整数的数组
int (&arrRef)[10]=arr;	//arrRef引用一个含有10个整数的数组

默认情况下,类型修饰符从右向左依次绑定,理解时最好用由内向外的顺序。

3.5.2 访问数组元素

数组的元素也能使用范围for语句或下标运算符来访问。数组的索引从0开始。

在使用数组下标的时候,通常将其定义为size_t类型。size_t是一种机器相关的无符号类型。在cstddef头文件中定义了size_t类型,这个文件是C标准库stddef.h头文件的C++语言版本。

数组除了大小固定这一特点外,其他用法与vector基本类似。

与vector和string一样,当需要遍历数组的所有元素时,最好的办法也是使用范围for语句。

检查下标的值

数组的下标是否在合理范围之内由程序员负责检查,所谓合理就是说下标应该大于等于0而且小于数组的大小。

3.5.3 指针和数组

使用数组的时候编译器一般会把它转换成指针。

对数组的元素使用取地址符就能得到指向该元素的指针。

string nums[]={"one","two","three"};	//数组的元素是string对象
string *p=&num[0];						//p指向nums的第一个元素

数组还有一个特性:在很多用到数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指针:

string *p2 = nums;//等价于p2 = &nums[0];

注:在大多数表达式中,使用数组类型的对象其实是使用一个指向该数组首元素的指针。

当使用数组作为一个auto变量的初始值时,推断得到的类型是指针而非数组,而当使用decltype关键字时,其返回的类型是数组而非指针。

指针也是迭代器

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

标准库函数begin和end

C++11新标准引入了两个名为begin和end的函数,这两个函数与容器中的两个同名成员功能类似。这两个函数定义在iterator头文件中。

指针运算

指向数组元素的指针可以执行所有迭代器的运算,这些运算用在指针和用在迭代器上意义完全一样。

和迭代器一样,两个指针相减的结果是它们之间的距离。参与运算的两个指针必须指向同一个数组当中的元素。

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

只要两个指针指向同一个数组的元素,或者指向该数组的尾元素的下一位置,就能利用关系运算符对其进行比较。如果两个指针分别指向不相关的对象,则不能比较它们。

解引用和指针运算的交互

指针加上一个整数所得的结果还是一个指针。假设结果指针指向了一个元素,则允许解引用该结果指针。最好在必要的地方加上圆括号。

下标和指针

在很多情况下使用数组的名字其实用的是一个指向数组首元素的指针。标准库类型限定使用的下标必须是无符号类型,而内置的下标运算无此要求。内置的下标运算符可以处理负值,结果地址必须指向原来的指针所指同一数组中的元素(或是同一数组尾元素的下一位置)。

3.5.4 C风格字符串

注:尽管C++支持C风格字符串,但在C++程序中最好还是不要使用它们。这是因为C风格字符串不仅用起来不太方便,而且极易引发程序漏洞,是诸多安全问题的根本原因。
字符串字面值是一种通用结构的实例,这种结构即是C++由C继承而来的C风格字符串(C-style character string)。C风格字符串不是一种类型,而是为了表达和使用字符串而形成的一种约定俗成的写法。按此习惯书写的字符串存放在字符数组中并以空字符结束(null terminated)

C标准库String函数

下标这些函数可用于操作C风格字符串,它们定义在cstring头文件中,cstring是C语言头文件string.h的C++版本。
C风格字符串的函数
传入此类函数的指针必须指向以空字符作为结束的数组。

比较字符串

比较两个C风格字符串的方法和之前学习过的比较标准库string对象的方法大相径庭。比较标准库string对象的时候,用的是普通的关系运算符和相等性运算符。

string s1 = "A string exanmple";
string s2 = "A different string";
if(s1<s2) 	//false:s2小于s1

如果把这些运算符用在两个C风格字符串上,实际比较的将是指针而非字符串本身:

const char ca1[] = "A string example";
const char ca2[] = "A different string";
if(ca1<ca2)		//未定义的:试图比较两个无关地址

要想比较两个C风格字符串需要调用strcmp函数,此时比较的就不再是指针了。如果两个字符串相等,strcmp返回0;如果前面的字符串较大,返回正值;如果后面的字符串较大,返回负值。

if(strcmp(ca1,ca2)<0)//和两个string对象的比较s1<s2效果一样
目标字符串的大小由调用者指定

连接或拷贝C风格字符串也与标准库string对象的同类操作差别很大。

//将largeStr初始化成s1、一个空格和s2的连接
string lagerStr = s1+" "+s2;
//如果我们计算错了largeStr的大小将引发严重错误
strcpy(largeStr,ca1);	//把ca1拷贝给largeStr
strcat(largeStr," ");	//在largeStr的末尾加上一个空格
strcat(largeStr,ca2);	//把ca2连接到largeStr后面

一个潜在的问题是,我们在估算largeStr后需的空间时不容易估准,而且largeStr所存的内容一旦改变,就必须重新检查其空间是否足够。
注:对大多数应用来说,使用标准库string要比使用C风格字符串更安全、更高效。

3.5.5 与旧代码的接口

任何出现字符串字面值的地方都可以用空字符结束的字符数组来替代:
· 允许使用以空字符结束的字符数组来初始化string对象或为string对象赋值。
· 在string对象的加法运算中允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个运算对象都是);在string对象的复合赋值运算中允许使用以空字符结束的字符数组作为右侧的运算对象。

上述性质反过来就不成立了。

如果程序的某处需要一个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()函数后程序像一只都能使用其返回的数组,最好将该数组重新拷贝一份。

使用数组初始化vector对象

允许数组来初始化vector对象。要实现这一目的,只需指明要拷贝区域的首元素地址和尾后地址就可以了:

int int_arr[]={0,1,2,3,4,5};
//ivec有6个元素,分别是int_arr中对应元素的副本
vector<int> ivec(begin(int_arr),end(int_arr));

用于初始化vector对象的值也可能仅是数组的一部分:

//拷贝三个元素,int_arr[1],int_arr[2],int_arr[3]
vector<int> subVec(int_arr+1,int_arr+4);

建议:尽量使用标准库类型而非数组
使用指针和数组很容易出错。一部分原因是概念上的问题:指针常用于底层操作,因此容易引发一些与繁琐细节有关的错误。其他问题则源于语法错误,特别是声明指针时的语法错误。
现代的C++程序应当尽量使用vector和迭代器,避免使用内置数组和指针;应该尽量使用string,避免使用C风格的基于数组的字符串。


3.6 多维数组

严格来说,C++语言中没有多维数组,通常所说的多维数组其实是数组的数组。

当一个数组的元素仍然是数组时,通常使用两个维度来定义它:一个维度表示数组本身大小,另外一个维度表示其元素(也是数组)大小:

int ia[3][4];//大小为3的数组,每个元素是含有4个整数的数组
//大小为10的数组,它的每个元素都是大小为20的数组
//这些数组的元素是含有30个整数的数组
int arr[10][20][30]={0};//将所有元素初始化为0

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

多维数组的初始化

允许使用花括号括起来的一组值初始化多维数组。

int ia[3][4] = {
		{0,1,2,3},
		{4,5,6,7},
		{8,9,10,11}
}
//没有标识每行的花括号,与之前的初始化语句等价的
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};
多维数组地下标引用

可以使用下标运算符来访问多维数组的元素,此时数组的每个维度对应一个下标运算符。

如果表达式含有的下标运算符数量和数组的维度一样多,该表达式的结果将是给定类型的数元素;反之,如果表达式含有的下标运算符数量比数组的维度小,则表达式的结果将是给定索引处的一个内层数组:

//用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 != rowCnt;++i){
	//对于行内的每一列
	for(size_t j = 0;j != colCnt;++j){
	//将元素的位置索引作为它的值
	ia[i][j]= i* conCnt+j;
	}
}
使用范围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)

程序将无法通过编译。因为row不是引用类型,所以编译器初始化row时会自动将这些数组形式的元素(和其他类型的数组一样)转换成指向该数组内首元素的指针。这样得到的row的类型就是int*,显然内层的循环就不合法了,编译器将试图在一个int*内遍历,这显然和程序的初衷相去甚远。
注:要使用范围for语句处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该时引用类型。

指针和多维数组

当程序使用多维数组的名字时,也会自动将其转换成指向数组首元素的指针。
注:定义指向多维数组的指针时,千万别忘了这个多维数组实际上是数组的数组。
因为多维数组实际上是数组的数组,所以由多维数组名转换得来的指针实际上是指向第一个内层数组的指针:

int ia[3][4];		//大小为3的数组,每个元素是含有4个整数的数组
int (*p)[4] = ia;	//p指向含有4个整数的数组
p = &ia[2];			//p指向ia的尾元素

注:在上述声明中,圆括号必不可少。

随着C++11新标准的提出,通过使用auto或者decltype就能尽可能地避免在数组前面加上一个指针类型。

当然,使用标准库函数begin和end也能实现同样的功能,而且看起来简洁一些。

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

读、写和理解一个指向多维数组的指针是一个让人不胜其烦的工作,使用类型别名就能让这项工作变得简单一点,如:

using int_array = int[4];//新标准下类型别名的声明
typedef int int_array[4];//等价的typedef声明
//输出ia中每个元素的值,每个内层数组各占一行
for(int_array *p = ia;p != ia + 3;++p){
	for(int *q = *p;q != *p+4;++q)
		cout<<*q<<' ';
	cout<<endl;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值