《C++ Primer(第5版)》第三章笔记

除了第2章介绍的内置类型之外,C++语言还定义了一个内容丰富的抽象数据类型库。其中,stringvector是两种最重要的标准库类型,前者支持可变长字符串,后者则表示可变长的集合。还有一种标准库类型是迭代器,它是string和vector的配套类型,常被用于访间string中的字符或vector中的元素。
内置数组是一种更基础的类型,stringvector都是对它的某种抽象。本章将分别介绍数组以及标准库类型string和vector。

3.1 命名空间using声明

目前为止,我们用到的库函数基本上都属于命名空间std,而程序也显式地将这一点标示了出来。例如,std::cin表示从标准输入中读取内容。此处使用作用域操作符(::)的含义是:编译器应从操作符左侧名字所示的作用域中寻找右侧那个名字。因此,std::cin的意思就是要使用命名空间std中的名字cin。
上面的方法显得比较烦琐,然而幸运的是,通过更简单的途径也能使用到命名空间中的成员。本节将学习其中一种最安全的方法,也就是使用using声明(using declaration)。

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

3.2 标准库类型string

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

3.2.1 定义和初始化string对象

string s1;				// 默认初始化,s1是一个空串
string s2(s1);			// s2是s1的副本
string s3 = s1;			// 等价于s2(s1),s2是s1的副本
string s4("value");		// s3是字面值"value"的副本,除了字面值最后的那个空字符外
string s5 = "value";	// 等价于s3("value"),s3是字面值"value"的副本
string s6(n, 'c');		// 把s4初始化为连续n个字符c自称的串

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

3.2.2 对象上的操作

一个类除了要规定初始化其对象的方式外,还要定义对象上所能执行的操作。其中,类既能定义通过函数名调用的操作,就像Sales_item类的isbn函数那样,也能定义<<、+等各种运算符在该类对象上的新含义。

在这里插入图片描述
读写string对象

也可以使用IO操作符读写是string对象
也可以连续输入输出

int main(){
	string s1, s1;
	cin >> s1 >> s2;
	cout << s1 << s2 << endl;
	return 0;
}

使用getline读取一整行

有时我们希望能在最终得到的字符串中保留输入时的空白符,这时应该用getline函数代替原来的>>运算符。getline函数的参数是一个输入流和一个string对象,函数从给定的输入流中读入内容,直到遇到换行符为止(注意换行符也被读进来了),然后把所读的内容存入到那个string对象中去(注意不存换行符)。getline只要一遇到换行符就结束读取操作并返回结果,哪怕输入的一开始就是换行符也是如此。如果输入真的一开始就是换行符,那么所得的结果是个空string。
和输入运算符一样, getline也会返回它的流参数。因此既然输入运算符能作为判断的条件,我们也能用getline的结果作为条件。例如,可以通过改写之前的程序让它一次输出一整行, 而不再是每行输出一个词了:

int maint(){
	string line;
	// 每次读取一行直至文件末尾
	while(getline(cin, line)) cout << line << endl;
	return 0;
}

string的empty和size操作
顾名思义,empty函数根据string对象是否为空返回一个对应的布尔值。

string line;
// 每次读入一整行,遇到空行直接跳过
while (getline(cin, line)) 
	if (!line.empty())
		cout << line << endl;

size函数返回string对象的长度(即string对象中字符的个数),可以使用seize函数只输出长度超过80ge的字符的行。

string line;
// 每次读入一整行,输出其中超过80个字符的行
whlie(getline(cin, line))
	if (line.size() > 80)
		cout << line << endl;

string::size_type类型
对于size函数来说,返回一个int一个unsigned似乎都是合情合理的。但其实size函数返回的是一个string::size_type类型的值。
string类及其他大多数标准库类型都定义了几种配套的类型。这些配套类型体现了标准库类型与机器无关的特性,类型size_type 即是其中的一种。在具体使用的时候,通过作用域操作符来表明名字size_type是在类string中定义的
尽管我们不太清楚string::size_type类型的细节,但有一点是肯定的:它是一个无符号类型的值而且能足够存放下任何string对象的大小。所有用于存放string类的size函数返回值的变量,都应该是string::size_type类型的。
过去,string::size_ type这种类型有点儿神秘,不太容易理解和使用。在C++11新标准中,允许编译器通过auto或者decltype来推断变量的类型:

auto len = line.size();		// len的类型是string::size_type

由于size函数返回的是一个无符号整型数,因此切记,如果在表达式中混用了带符号数和无符号数将可能产生意想不到的结果。例如,假设n是一个具有负值的int,则表达式s.size()<n的判断结果几乎肯定是true。这是因为负值n会自动地转换成一个比较大的无符号值。

比较string对象

string类定义了集中用于比较字符串的运算符。这些比较运算符逐一比较string对象中的字符,并且对大小写敏感。

  1. 如果两个string对象的长度不同,而且较短string对象的每个字符都与较长string对象对应位置上的字符相同,就说较短string对象小于较长string对象。
  2. 如果两个string对象在某些对应的位置上不一致,则string对象比较的结果其实是string对象中第一对相异字符比较的结果。

string对象赋值

一般来说, 在设计标准库类型时都力求在易用性上向内置类型看齐,因此大多数库类型都支持赋值操作。对于string类而言,允许把-一个对象的值赋给另外-一个对象:

string st1(10, 'c'), st2;	// st1的内容是ccccccccc; st2是一个空字符串
st1 = st2;					// 赋值:用st2的副本替换st1的内容
							// 此时st1和st2都是空字符串

两个string对象相加

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

string s1 = "hello, ", s2 = "world\n";
string s3 = s1 + s2;			// s3的内容是hello, world\n
s1 += s2;						// 等价于 s1 = s1 + s2;

字面值和string对象相加
即使是一种类型并非所需,我们也可以只用它,不过前提是该种类型可以自动转换成所需的类型。因为标准库允许吧字符字面值和字符串字面值转换成string对象,所以在需要string对象的地方就可以使用这两种字面值来替代。利用这一点,将之前的程序改为如下形式:

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

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

string s5 = "hello" + ", ";			// 错误:两个运算对象都不是string,不能把字面值直接相加
string s7 = "hello" + ", " + s2;	// 错误

处理string对象中的字符

有时需要处理string对象中的每一个字符,另外一些时候则只需处理某个特定的字符,还有些时候遇到某个条件处理就要停下来。以往的经验告诉我们,处理这些情况常常要涉及语言和库的很多方面。另一个关键问题是要知道能改变某个字符的特性。在cctype头文件中定义了一组标准库函数处理这部分工作,下表列出了主要的函数名及其含义:
在这里插入图片描述
建议使用C++版本的C标准头文件
C++标准库中除了定义C++语言特有的功能外,也兼容了C语言的标准库。C语言的头文件形如name.h,C++则将这些文件命名为cname。也就是去掉了.h后缀,而在文件名name之前添加了字母c,这里的c表示这是一个属于C语言标准库的头文件。
因此,cctype头文件和ctype.h头文件的内容是一样的,只不过从命名规范上来讲更符合C++语言的要求。特别的,在名为cname的头文件中定义的名字从属于命名空间std,而定义在名为.h的头文件中的则不然。

处理每个字符?使用基于范围的for语句
范围for语句:这种语句遍历给定序列中的每个元素并对序列的每个值执行某种操作:

for (declaration : expression)
	statement

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

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

通过auto关键字让编译器来决定变量c的类型,这里的c的类型是char

举个稍微复杂些的例子,使用范围for语句和ispunct函数来统计string对象中标点符号的个数:

string s("Hello World!!!");
decltype(s.size()) punct_cnt = 0;	// punct_cnt类型和s.size()的返回类型一样
// 统计s中标点符号的个数
for (auto c : s)			// 对于s中的每个字符
	if (ispunct(c))
		++punct_cnt;
cout << punct_cnt << " punctuation characters in " << s << endl;

程序的输出结构是:
3 punctuation characters in Hello World!!!

使用范围for语句改变字符串中的字符

如果想要改变string对象中字符的值,必须把循环变量定义成引用类型。记住,所谓引用只是给定对象的一个别名,因此当使用引用作为循环控制变量时,这个变量实际上被依次绑定到了序列的每个元素上。使用这个引用,我们就能改变它绑定的字符。

假设我们想要吧字符串改写为大写字母的形式。为了做到这一点可以使用标准库函数toupper,该函数接受一个字符,然后输出其对应的大写形式。

string s(Hello World!!!);
for (auto &c : s)			// 对于s中的每个字符(注意:c是引用)
	c = toupper(c);			// c是一个引用,因此赋值语句将改变s中字符的值
cout << s << endl;			// 输出 HELLO WORLD!!!

只处理一部分字符?

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

下标运算符([ ]) 接收的输入参数是string::size_type类型的值,这个参数表示要访问的字符的位置;返回值是该位置上字符的引用注意检查下标的合法性

3.3 标准库类型vector

标准库类型vector表示对象的集合,其中所有对象的类型都相同。集合中的每个对象都有一个与之对应的索引,索引用于访问对象。因为vector“容纳着”其他对象,所以它也常被称作容器(container)。

C++语言既有类模板也有函数模板,其中vector是一个类模板
模板本身不是类或函数,相反可以将模板看作为编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程称为实例化(instantiation),当使用模板时,需要指出编译器应把类或函数实例化成何种类型。
对于类模板来说,我们通过提供一些额外信息来指定模板到底实例化成什么样的类,需要提供哪些信息由模板决定。提供信息的方式总是这样:即在模板名字后面跟一对尖括号,在括号内放上信息。
vector是模板而并非类型,由vector生成的类型必须包含vector中元素的类型,例如vector<int>

vector<int> ivec;
vector<Sales_item> Sales_vec;
vector<vector<string>> file;

3.3.1 定义和初始化vector对象

在这里插入图片描述
列表初始化vector对象

用花括号括起来的0个或多个出事元素值被赋给vector对象:

vector<string> v1 = {"a", "an", "the"};
vector<string> v2{"a", "an", "the"};
vector<string> v3("a", "an", "the");	// 错误

创建指定数量的元素

vector<int> ivec(10, -1);		// 10个int类型的元素,每个都被初始化为-1
vector<string> svec(10, "hi!");	// 10个string类型的元素,每个都被初始化为"hi!"

值初始化
只提供vector对象容纳的元素数量而不用初始值。此时库会创建一个值初始化元素初值,并把它赋给容器中所有元素。这个初值有vector对象中的元素类型决定:

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

列表初始化还是元素数量?
如果用的是花括号,可以表述成我们想列表初始化(list initialize)该vector对象。也就是说,初始化过程会尽可能地把花括号内的值当成是元素初始值的列表来处理,只有在无法执行列表初始化时才会考虑其他初始化方式
另一方面,如果初始化时使用了花括号的形式但是提供的值又不能用来列表初始化,就要考虑用这样的值来构造vector对象了。例如,要想列表初始化一个含有string对象的vector对象,应该提供能赋给string对象的初值。此时不难区分到底是要列表初始化vector对象的元素还是用给定的容量值来构造vector对象:

vector<string> v5{"hi"};		// 列表初始化:v5有一个元素
vector<string> v6("hi");		// 错误:不能用字符串字面值构建vector对象
vector<string> v7{10};			// v7有10个默认初始化的元素(10个空string)
vector<string> v8{10, "hi"};	// v8有10个值为"hi"的元素

3.3.2 向vector对象中添加元素

多数情况是:创建一个vector对象时并不清楚实际所需的元素个数,元素的值也经常无法确定。还有一种情况是即使元素初值已知,但如果这些值总量较大且各不相同,那么创建时初始化也过于繁琐。

这种情况下更好地处理方法是创建一个空vector,然后在运行时再利用push_back向其中添加元素。

vector<int> v2;
for (int i= 0; i != 100; ++i)
	v2.push_back(i);

如果运行时才知道对象的个数

string word;
vector<string> text;
while(cin >> word) {
	text.push_back(word);	// 把word添加到text后面
}

vector对象能高效增长

C++标准要求vector应该能在运行时高效快速地添加元素。因此既然vector对象能高效地增长,那么在定义vector对象的时候设定其大小也就没什么必要了,事实上如果这么做性能可能更差。只有一种例外情况,就是所有元素的值都一样。一旦元素的值有所不同,更有效的办法是先定义一个空的vector对象,再在运行时向其中添加具体值。此外,9.4节将介绍,vector还提供了方法,允许我们进一步提升动态添加元素的性能。
开始的时候创建空的vector对象,在运行时再动态添加元素,这一做法与C语言及其他大多数语言中内置数组类型的用法不同。特别是如果用惯了C或者Java,可以预计在创建vector对象时顺便指定其容量是最好的。然而事实上,通常的情况是恰恰相反。

向vector对象添加元素蕴含的编程假定
由于能高效便捷地向vector对象中添加元素,很多编程工作被极大简化了。然而,这种简便性也伴随着一些对编写程序更高的要求:其中一条就是必须要确保所写的循环if确无误,特别是在循环有可能改变vector对象容量的时候。
随着对vector的更多使用,我们还会逐渐了解到其他一些隐含的要求, 其中一条是现在就要指出的:如果循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环。

3.3.3 其他vector操作

在这里插入图片描述
访问vector对象中元素的方法和访问string对象中字符的方法差不多

vector<int> v{1, 2, 3, 4, 5};
for (auto &i : v)
	i *= i;
for (auto i : v)
	cout << i  << " ";
cout << endl;

vector的size成员与string的同名成员功能完全一致。size返回值类型是由vector定义的size_type类型。

vector<int>::size_type		// 正确
vector::size_type			// 错误

只有在元素的值可比时,vector对象才能被比较。比如一些类,如string等,确实定义了自己的相等性运算符和关系运算符;另一些如目前的Sales_item类显然并不支持相等性判断和关系运算等操作,因此不能比较两个vector<Sales_item>对象。

不能用下标形式添加元素

vector<int> ivec;
for (decltype(ivec.size()) ix = 0; ix != 10; ++ix)
	ivec[ix] = ix;		// 严重错误:ivec并不包含任何元素

只能对已知存在的元素执行下标操作,确保下标合法的一种有效手段就是尽可能使用范围for语句。

3.4 迭代器介绍

我们已经知道可以使用下标运算符来访问string对象的字符或vector对象的元素,还有另外一种更通用的机制也可以实现同样的目的,这就是迭代器(iterator)。 严格来说,string对象不属于容器类型,但是string支持很多与容器类型类似的操作。vector支持下标运算符,这点和string一样;string支持迭代器,这也和vector是一样的。

迭代器也提供了对对象的间接访问。就迭代器而言,其对象是容器中的元素或者string对象中的字符。使用迭代器可以访问某个元素,迭代器也能从一个元素移动到另外一个元素。迭代器有有效和无效之分,这一点和指针差不多。有效的迭代器或者指向某个元素,或者指向容器中尾元素的下一位置:其他所有情况都属于无效。

3.4.1 使用迭代器

容器类型都拥有名为beginend的成员,being负责返回执行第一个元素的迭代器,end负责返回指向容器(或string对象)“尾元素的下一个位置”。如果容器为空,则being和end返回的是同一个迭代器。
一般来说我们不清楚(不在意)迭代器的准确类型到底是什么。

auto b = v.begin(), e = v.end();

迭代器运算符
在这里插入图片描述
之前我们实现了利用下标运算符把string对象的第一个字母改成了大写形式,下面利用迭代器实现同样的功能:

string s("some string");
if (s.being() != s.end()){		// 确保s非空
	auto it = s.begin();
	*it = toupper(*it);			// Some string
}

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

迭代器利用递增(++)运算符来从一个元素移动到下一个元素。
之前有一个程序把string对象中第一个单词改写为大写形式,现在利用迭代器及其递增运算符可以实现相同的功能:

// 一次处理s的字符直至我们处理完全部字符或遇到空白
for (auto it = s.begin(); it != end() && !isspace(*it); it++)
	*it = toupper(*it);

泛型编程

原来使用C或Java的程序员在转而使用C++语言之后,会对for循环中使用!=而非<进行判断有点儿奇怪,比如上面的这个程序。C++程序员习惯性地使用!=,其原因和他们更愿意使用迭代器而非下标的原因一样:因为这种编程风格在标准库提供的所有容器上都有效。
之前已经说过,只有string和vector等一些标准库类型有下标运算符,而并非全都如此。与之类似,所有标准库容器的迭代器都定义了==!=,但是它们中的大多数都没有定义<运算符。因此,只要我们养成使用迭代器和!=的习惯,就不用大在意用的到底是哪种容器类型。

迭代器类型

那些拥有迭代器的标准库类型使用iteratorconst_iterator类表示迭代器类型:

vector<int>::iterator it;			// it1能读写元素
string::iterator it2;				

vector<int>::const_iterator it3;	// it3只能读元素,不能写元素
string::const_iterator it4;			

const_iterator和常量指针差不多,能读取但不能修改他所指的元素;相反iterator的对象可读可写。

begin和end运算符

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

vector<int> v;
const vector<int> cv;
auto it1 = v.begin();	// iterator
auto it2 = cv.begin();	// const_iterator

如果需要专门得到const_iterator类型,可使用C++11新标准引入的cbegin()cend()

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

解引用迭代器可获得迭代器所指的对象,如果该对象的类型恰好是类,就有可能希望进一步访问它的成员。例如,对于一个由字符串组成的vector对象来说,要想检查其元素是否为空,令it是该vector对象的迭代器,只需检查it所指字符串是否为空就可以了,其代码如下所示:

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

为了简化上述表达式,C++语言定义了箭头运算符(->)。 箭头运算符把解引用和成员访问两个操作结合在一起, 也就是说,it->empty()(*it).empty()表达的意思相同。
例如:假设用一个名为text的字符串向量存放文本文件中的数据,其中的元素或者是一句话或者是一个用于表示段落分隔的空字符串。如果要输出text中第一段的内容,可以利用迭代器写一个循环令其遍历text,直到遇到空字符串的元素为止:

// 依次输出text的每一行直至遇到第一个空白行为止
for (auto it = text.cbegin(); it != text.cend() && !it->empty(); ++it)
	cout << *it << endl;

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

虽然vector对象可以动态的增长,但是也会有一些副作用。一直的一个限制是不能再范围for循环中间想vector对象添加元素。另外一个限制是任何一种可能改变vector对象容量的操作,比如push_back,都会使vector队形的迭代器失效

3.4.2 迭代器运算

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

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

// 计算得到距离中间最近的迭代器
auto mid = vi.begin() + vi.size() / 2;

对于string或vector的迭代器来说,除了判断是否相等,还能使用关系运算符(<、<=、>、>=)对其进行比较。参与比较的两个迭代器必须合法而且指向的是同一个容器的元素(或者尾元素的下一位置)。例如,假设it和mid是同一个vector对象的两个迭代器,可以用下面的代码来比较它们所指的位置孰前孰后:

if (it < mid)
	// 处理部分

只要两个迭代器指向的是同一个容器中的元素或者尾元素的下一位置,就能将其相减,所得结果是两个迭代器的距离。其类型是名为difference_type带符号整型数。字符串和vector都定义了difference_type。

使用迭代器运算

使用迭代器运算的一个经典算法是二分搜索。二分搜索从有序序列中寻找某个给定的值。二分搜索从序列中间的位置开始搜索,如果中间位置的元素正好就是要找的元素,搜索完成;如果不是,假如该元素小于要找的元素,则在序列的后半部分继续搜素:假如该元素大于要找的元素,则在序列的前半部分继续搜索。在缩小的范围中计算一个新的中间元素并重复之前的过程,直至最终找到目标或者没有元素可供继续搜索:

// text必须有序
// beg和end表示搜索范围
auto beg = text.begin(), end = text.end();
auto mid = text.begin() + (text.begin() + text.end()) / 2;	// 初始状态下的中间点
whlie(mid != end && *mid != sought) {
	if (sought < *mid)
		end = mid;
	else
		beg = mid + 1
	mid = beg + (end - beg) / 2;				// 新的中间点
}

3.5 数组

数组是一种类似于标准库类型vector的数据结构,但是在性能和灵活性的权衡上又与vector有所不同。
与vector相似的地方是,数组也是存放类型相同的对象的容器,这些对象本身没有名字,需要通过其所在位置访问。
与vector不同的地方是,数组的大小确定不变,不能随意向数组中增加元素。因为数组的大小固定,因此对某些特殊的应用来说程序的运行时性能较好,但是相应地也损失了一些灵活性。

3.5.1 定义和初始化内置数组

数组是一种复合类型。数组的声明形如a[d],其中a是数组的名字,d是数组的维度。维度说明了数组中元素的个数,因此必须大于0。数组中元素的个数也属于数组类型的一部分, 编译的时候维度应该是已知的。也就是说,维度必须是一个常量表达式:

int a = 10;
constexpr int b = 10;
int arr[a];		// 错误:a不是常量表达式
int arr[b];		// 正确

定义数组时必须指定数组的类型,不允许用auto关键字有初始值的裂变推断类型。另外和vector一样,数组的元素应为对象,因此不存在引用的数组

int &brr = arr;		// 错误

显示初始化数组元素

const unsigned sz = 3;
int ial[sz] = {0, 1, 2};		// {0, 1, 2}
int a2[] = {0, 1, 2};			// {0, 1, 2}
int a3[5] = {0, 1 ,2}			// {0, 1, 2, 0, 0}
string a4[3] = {"hi", "bye"};	// {"hi", "bye", ""}
int a5[2] = {0, 1 ,2};			// 错误:初始值过多

字符数字的特殊性

可以使用字符串字面值对此类数组初始化。结尾会自动添加空字符,如果没有空间添加则错误:

char a1[] = "C++";				// 自动添加结尾空字符
const char a2[6] = "Daniel";	// 错误:没有空间放空字符
char a3[] = {'C', '+', '+'};	// 结尾没有空字符
char a4[] = {'C', '+', '+', '\0'};	// 有空字符

不允许拷贝和赋值

不能将数组的内容拷贝给其他数组做初值,也不能用数组给其他数组赋值:

理解复杂的数组声明

定义数组的指针或数组的引用较为复杂:

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

对于Parray从内向外阅读较好,*Parray是一个指针,是指向含有10个整型变量的指针。arrRef、ParrRef也同样。

3.5.2 访问数组元素

使用范围for语句或下标运算符来访问。
使用数组下标时,通常将其定义为size_t类型(无符号类型)。

检查下标的值

数组下标是否在合理范围之内由程序员负责检查,应大于0小于数组大小。

3.5.3 指针和数组

使用数组时编译器一般会把它转换成指针,编译器会自动的将数组的名字替换成一个纸箱数组首元素的指针:

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

由上可知,在一些情况下数组的操作实际上是指针的操作,这一结论有很多隐含的意思。其中一层意思是当使用数组作为一个auto变量的初始值时,推断得到的类型是指针而非数组:

int ia[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};		// ia是一个含有10整数的数组
auto ia2(ia);	// ia2是一个整型指针,指针ia的第一个元素
ia2 = 42;		// 错误:ia2是一个指针,不能用int值给指针赋值

尽管ia是由10个整数构成的数组,但当使用ia作为初始值时,编译器实际执行的初始化过程类似于下面的形式:

auto ia2(&ia[0]);

必须指出的是,当使用decltype关键字时上述转换不会发生,decltype(ia)返回的类型是由10个整数构成的数组:

// ia3和ia一样,是一个含有10整数的数组
decltype(ia) ia3 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};	
ia3 = p2;	// 错误:不能用整形指针给数组赋值
ia3[4] = i;

指针也是迭代器

与之前介绍的迭代器相比,指向数组元素的指针拥有更多功能。vector和string的迭代器支持的运算,数组的指针全都支持。例如,允许使用递增运算符将指向数组元素的指针向前移动到下一个位置上。
我们可以通过数组名字获取数组首元素地址,不过获取尾后指针就要用到数组的另外一个特殊性质了。我们可以获取设法获取数组尾元素之后的那个并不存在的与那U盾的地址:

int *e = &arr[10];	// 指向尾元素的下一个位置(arr有10个元素)

这里显然使用下标运算符索引了一个不存在的元素,arr有10个元素,尾元素所在位置的索引是9,接下来那个不存在的元素唯一的用处就是提供其地址用于初始化e。就像尾后迭代器一样,尾后指针也不指向具体的元素。因此,不能对尾后指针执行解引用或递增的操作

标准库begin和end

尽管能计算得到尾后指针,但这种用法极易出错。为了让指针的使用更简单、更安全,C++11新标准引入了两个名为beginend的函数。这两个函数与容器中的两个同名成员功能类似,不过数组毕竟不是类类型,因此这两个函数不是成员函数。正确的使用形式是将数组作为它们的参数

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

这两个函数定义在iterator头文件中。

一个指针如果指向了某种内置类型数组的尾元素的“下一位置”,则其具备与vector的end函数返回的与迭代器类似的功能。特别要注意,尾后指针不能执行解引用和递增操作。

指针运算

指向数组元素的指针可以所有迭代器运算。这些运算,包括解引用、递增、比较、与整数相加、两个指针相减等,用在指针和用在迭代器上意义完全一致。

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

auto n = end(arr) - begin(arr);

可以使用以下方式遍历数组中的元素:

int *b = arr, *e = arr + sz;
while (b < e){
	cout << *b << endl;
	b++;
}

下标和指针
如前所述,在很多情况下使用数组的名字其实用的是一个指向数组首元素的指针。个典型的例子是当对数组使用下标运算符时,编译器会自动执行上述转换操作。给定

int ia[] = {0, 2, 4, 8};
int i = ia[2];

int *p = ia;
i = *(p + 2);	// 等价于i = ia[2];

只要指针指向的是数组中的元素(或者数组中尾元素的下一位置),都可以执行下标运算,哪怕是负数:

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

3.5.5 与旧代码的接口

很多C++程序在标准库出现之前就已经写成了,它们肯定没用到string和vector类型。而且,有一些C++程序实际上是与C语言或其他语言的接口程序,当然也无法使用C++标准库。因此,现代的C++程序不得不与那些充满了数组和/或C风格字符串的代码衔接,为了使这一工作简单易行,C++专门提供了一组功能。

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

  • 允许使用以空字符结束的字符数组来初始化string对象或为string对象赋值。
  • 在string对象的加法运算中允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个运算对象都是)
  • 在string对象的复合赋值运算中允许使用以空字符结束的字符数组作为右侧的运算对象。

上述性质反过来就不成立了,如果程序某处需要使用一个C风格的字符串,string专门提供了一个名为c_str的成员函数:

char *str = s;		// 错误:不能用string对象初始化char*
const char *str = s.c_str();	// 正确

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

使用数组初始化vector对象

int int_arr = {0, 1, 2};
vector<int> ivec(being(int_arr), end(int_arr));

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 ib[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
// 显式的初始化每行的首元素,其余为0
int ic[3][4] = {{0}, {4}, {8}};
// 显式的初始化首行,其余为0
int id[3][4] = {0, 3, 6, 9}

指针和多维数组
当程序使用多维数组名字是,也会自动将其转换成指向数组首元素的指针。
定多维数组时千万别忘了这个多维数组实际上是数组的数组

因为多维数组实际上是数组的数组,所以多维数组名转换得来的指针实际上是指向第一个内层数组的指针:

int ia[3][4];
int (*p)[4] = ia;	// p指向含有4个整数的数组
p = &ia[2];			// p指向ia的尾元素(数组)
int *ip[4]			// 整型指针的数组

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

for(auto p = ia; p != ia + 3; ++p){
	for (auto q = *p; q != *p + 4; ++q){
		cout << *q << " ";
	}
	cout << endl;
}

当然,使用标准库函数beginend也能实现同样的功能,而且看起来更简洁一些:

for (auto p = begin(ia); p != end(ia); ++p){
	for (auto q = begin(*p); q != end(*p); ++q){
		cout << *q << " ";
	}
	cout << endl;
}

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

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

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;
}

程序将类型“4个整数组成的数组”命名为itn_array,用类型名int_array定义外层循环的控制变量让程序显得简洁明了。

小结

一般来说,应该优先选用标准库提供的类型,之后再考虑C++语言内置的低层的替代品数组或指针

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值