目录
第3章 字符串、向量和数组
3.1 命名空间的using声明
3.2 标准库类型string
1)定义和初始化string对象
2)string对象上的操作
3)处理string对象中的字符
3.3 标准库类型vector
1)定义和初始化vector对象
2)向vector对象中添加元素
3)其他vector操作
3.4 迭代器介绍
1)使用迭代器
2)迭代器运算
3.5 数组
1)定义和初始化内置数组
2)访问数组元素
3)指针和数组
4)C风格字符串
5)与旧代码的接口
3.6 多维数组
作用域操作符(::)的含义是:编译器应从操作符左侧名字所示的作用域中寻找右侧那个名字。因此,std::cin的意思就是要使用命名空间std中的名字cin。
使用using
声明后,就无须再通过专门的前缀去获取所需的名字了。using
声明具有如下的形式:using namespace::name。
举个例子:
程序中使用的每个名字都需要用独立的using
声明引入,或者需要引入 spacename。
头文件中通常不应该包含 using 声明。这是因为头文件的内容会拷贝到所有引用它的文件巾去,如果头文件里有某个 using 声明,那么每个使用了该头文件的文件就都会有这个声明。对于某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突。
标准库类型 string 表示可变长的字符序列,定义在头文件 string 中。
初始化 string 的方式:
如果 使用等号 初始化一个变量,实际上执行的是 拷贝初始化(copy initialization),编译器把等号右侧的初始值拷贝到新创建的对象中去。如果 不使用等号,则执行的是 直接初始化(direct initialization)。
string 的操作:
在执行读取操作时,string
对象会自动忽略开头的空白(空格符、换行符、制表符等)并从第一个真正的字符开始读取,直到遇见下一处空白为止。
注意:_Hello_World_,输入的时候有三个空格(这里用三个下划线标注出来),分别在前面、中间和后面,但是输出的时候没有任何空格。
当设置多个输入或者输出时,可以连写在一起。
使用getline 函数可以读取一整行字符
当希望能在输出的字符串中保留输入的空白字符时,可以使用 getline 函数,getline
函数可以读取一整行字符。该函数只要遇到换行符就结束读取并返回结果(换行符也被读进来了),然后把所读内容存入到 string
对象中(不保存换行符)。如果输入的开始就是一个换行符,则得到空 string
。
触发
getline
函数返回的那个换行符实际上被丢弃掉了,得到的string
对象中并不包含该换行符。
string的empty和size操作
empty
函数根据 string对象是否为空返问一个对应的布尔值;size 函数返回string对象的长度(即string对象中字符的个数),返回值其实是 string::size_type 类型,这是一种无符号类型。要使用 size_type,必须先指定它是由哪种类型定义的。
如果一个表达式中已经有了 size 函数就不要再使用 int 了,这样可以避免混用 int 和 unsigned int 可能带来的问题。
当把 string对象和字符字面值及字符串字面值混合在一条语句中使用时,必须确保每个加法运算符两侧的运算对象中至少有一个是 string。
为了与C兼容,C++语言中的字符串字面值并不是标准库 string 的对象。 切记,字符串字面值与 string 是不同的类型。
头文件 cctype 中的字符操作函数:
建议使用C++版本的C标准库头文件。C语言中名称为 name.h 的头文件,在C++中则被命名为 cname。
C++11提供了范围 for(range for)语句,可以遍历给定序列中的每个元素并执行某种操作。
for (declaration : expression)
statement
其中,expression 部分是一个对象,用于表示一个序列。declaration 部分负责定义一个变量,该变量被用于访问序列中的基础元素。每次迭代,declaration 部分的变量都会被初始化为 expression 部分的下一个元素值。
如果想在范围 for 语句中改变 string 对象中字符的值,必须把循环变量定义成引用类型。
下标运算符接收的输入参数是 string::size_type 类型的值,表示要访问字符的位置,返回值是该位置上字符的引用。
string 对象的下标必须从0记起,范围是0至 size - 1,左闭右开。使用超出范围的下标将引发不可预知的后果。
C++标准并不要求标准库检测下标是否合法。编程时可以把下标的类型定义为相应的 size_type,这是一种无符号数,可以确保下标不会小于0,此时代码只需要保证下标小于 size 的值就可以了。另一种确保下标合法的有效手段就是使用范围 for 语句。
标准库类型 vector
表示对象的集合,也叫做 容器(container),定义在头文件 vector
中。vector
中所有对象的类型都相同,每个对象都有一个索引与之对应并用于访问该对象。
vector
是一个 类模板(template),模板本身不是类或函数,相反可以将模板看作为编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程,称为 实例化(instantiation),当使用模板时,需要指出编译器应把类或函数实例化成何种类型。
vector是模板而非类型,由 vector生成的类型必须包含 vector中元素的类型,如 vector
因为引用不是对象,所以不存在包含引用的 vector。
在早期的C++标准中,如果vector的元素还是 vector(或者其他模板类型),定义时必须在外层 vector对象的右尖括号和其元素类型之间添加一个空格,如 vector
初始化 vector 对象的方法:
最常见的方式就是先定义一个空vector,然后当运行时获取到元素的值后再逐一添加。
初始化 vector 对象时:
可以只提供 vector对象容纳的元素数量而省略初始值,此时会创建一个值初始化(value-initialized)的元素初值,并把它赋给容器中的所有元素。这个初值由 vector对象中的元素类型决定。
对vector对象来说,直接初始化的方式适用于三种情况:
push_back 函数可以把一个值添加到 vector的尾端。
范围 for 语句体内不应该改变其所遍历序列的大小。
vector 支持的操作:
vector和 string对象的下标运算符只能用来访问已经存在的元素,而不能用来添加元素。
正确的添加方法是使用 push_back:
访问vector对象中的元素的方法和访问 string对象中字符的方法差不多,也是通过元素在vector对象中的位置。例如:
迭代器的作用和下标运算类似,但是更加通用。所有标准库容器都可以使用迭代器,但是其中只有少数几种同时支持下标运算符。迭代器的作用也和指针类型类似,也提供了对对象的间接访问。
定义了迭代器的类型都拥有 begin和 end两个成员函数。begin函数返回指向第一个元素的迭代器,end函数返回指向容器 尾元素的下一位置(one past the end) 的迭代器。
end函数 通常被称作 尾后迭代器(off-the-end iterator) 或者简称为 尾迭代器(end iterator)。
如果容器为空,则 begin和 end返回的是同一个迭代器,都是尾后迭代器。
因为 end返回的迭代器并不实际指向某个元素,所以不能对其进行递增或者解引用的操作。
在 for或者其他循环语句的判断条件中,最好使用 !=而不是 <。所有标准库容器的迭代器都定义了 == 和 !=,但是只有其中少数同时定义了 < 运算符。
迭代器类型
如果 vector 或 string 对象是常量,则只能使用 const_iterator 迭代器,该迭代器只能读元素,不能修改元素。
begin和 end返回的迭代器具体类型由对象是否是常量决定,如果对象是常量,则返回 const_iterator;如果对象不是常量,则返回 iterator。
C++11新增了 cbegin
和 cend
函数,不论 vector 或 string 对象是否为常量,都返回 const_iterator 迭代器。
不能在范围for循环中向vector对象添加元素。
任何可能改变vector对象容量的操作,比如push_back,都会使该对象的迭代器失效。
vector 和 string 迭代器支持的操作:
假设it和mid是同一个vector对象的两个迭代器。可以用下面的代码来比较它们所指向的位置孰前孰后。
if ( it < mid)
// 处理Vi前半部分的元素
只要两个迭代器指向的是同一个容器中的元素或者尾元素的下一位置,就能将其相减,所得结果是两个送代器的距离。所谓距离,是右侧的迭代器向前移动多少位置就能追上左侧的迭代器,difference_type
类型用来表示两个迭代器间的距离,这是一种带符号整数类型。
使用迭代器运算:
使用迭代器完成了二分搜索:
数组类似 vector,也是存放类型相同的对象的容器,但数组的大小确定不变,不能随意向数组中添加元素。如果不清楚元素的确切个数,应该使用 vector。
数组是一种复合类型,声明形式为 a[d],其中 a 是数组名称,d 是数组维度。维度说明了数组中元素的个数,因此必须大于0。数组中元素的个数也属于数组类型的一部分,编译的时候维度应该是己知的,即维度必须是一个常量表达式。
默认情况下,数组的元素被默认初始化。
定义数组的时候必须指定数组的类型,不允许用auto
关键字由初始值列表推断类型。
如果定义数组时提供了元素的初始化列表,则允许省略数组维度,编译器会根据初始值的数量计算维度。但如果显式指明了维度,那么初始值的数量不能超过指定的大小。如果维度比初始值的数量大,则用提供的值初始化数组中靠前的元素,剩下的元素被默认初始化。
可以用字符串字面值初始化字符数组,但字符串字面值结尾处的空字符也会一起被拷贝到字符数组中。
不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值:
从数组的名字开始由内向外阅读有助于理解复杂数组声明的含义。
数组下标通常被定义成 size_t
类型,这是一种机器相关的无符号类型,可以表示内存中任意对象的大小。size_t
定义在头文件 cstddef
中。
数组除了大小固定这一特点外,其他用法与 vector
基本类似。
大多数常见的安全问题都源于缓冲区溢出错误。当数组或其他类似数据结构的下标越界并试图访问非法内存区域时,就会产生此类错误。
在大多数表达式中,使用数组类型的对象其实是在使用一个指向该数组首元素的指针。
一维数组寻址公式:
当使用数组作为一个auto
变量的初始值时,推断得到的类型是指针而非数组。但 decltype
关键字不会发生这种转换,直接返回数组类型。
C++11在头文件 iterator 中定义了两个名为 begin 和 end 的函数,功能与容器中的两个同名成员函数类似,参数是一个数组。
特别注意,后指针不能执行解引用和递增操作。
给(从)一个指针加上(减去)某整数值,结果仍是指针。两个指针相减的结果类型是ptrdiff_t,这是一种定义在头文件 cstddef 中的带符号类型。因为差值可能为负值,所以 ptrdiff_t是一种带符号类型。
表达式 * (ia+4) 计算 ia 前进4个元素后的新地址,解引用该结果指针的效果等价于表达式 ia[4]。
标准库类型限定使用的下标必须是无符号类型,而内置的下标运算无此要求。
尽管C++支持C风格字符串,但在C++程序中最好还是不妥使用它们。这是因为C风格字符串不仅使用起来不太方便,而且极易引发程序漏洞, 是诸多安全问题的根本原因。
字符串字面值是一种通用结构的实例,这种结构即是C++由C继承而来的 C风格字符串(C-style character string)。C风格字符串是将字符串存放在字符数组中,并以 空字符 结束(null terminated)。这不是一种类型,而是一种为了表达和使用字符串而形成的约定俗成的书写方法。以空字符结束的意思是在字符串最后一个字符后面跟着一个空字符 \0。
一般利用指针来操作这些字符串。
C风格字符串函数不负责验证其参数的正确性,传入此类函数的指针必须指向以 空字符 作为结尾的数组。
对大多数程序来说,使用标准库string要比使用C风格字符串更加安全和高效。
任何出现字符串字面值的地方都可以用以空字符结束的字符数组来代替:
不能用string对象直接初始化指向字符的指针。为了实现该功能,string提供了一个名为 c_str
的成员函数,返回 const char*类型的指针,指向一个以空字符结束的字符数组,数组的数据和 string对象一样。
针对 string对象的后续操作有可能会让c_str
函数之前返回的数组失去作用,如果程序想一直都能使用其返回的数组,最好将该数组重新拷贝一份。
可以使用数组来初始化 vector
对象,但是需要指明要拷贝区域的首元素地址和尾后地址。
在新版本的C++程序中应该尽量使用 vector、string 和迭代器,避免使用内置数组、C风格字符串和指针。
C++中的多维数组其实就是数组的数组。当一个数组的元素仍然是数组时,通常需要用两个维度定义它:一个维度表示数组本身的大小,另一个维度表示其元素(也是数组)的大小。通常把二维数组的第一个维度称作行,第二个维度称作列。
多维数组初始化的几种方式:
可以使用下标访问多维数组的元素,数组的每个维度对应一个下标运算符。
多维数组寻址公式:
使用范围 for 语句处理多维数组时,为了避免数组被自动转换成指针,语句中的外层循环控制变量必须声明成引用类型。
如果 row 不是引用类型,编译器初始化 row 时会自动将数组形式的元素转换成指向该数组内首元素的指针,这样得到的 row 就是 int* 类型,而之后的内层循环则试图在一个 int* 内遍历,程序将无法通过编译。
for (auto row : ia)
for (auto col : row)
使用范围 for 语句处理多维数组时,除了最内层的循环,其他所有外层循环的控制变量都应该定义成引用类型。
因为多维数组实际上是数组的数组,所以由多维数组名称转换得到的指针指向第一个内层数组。
声明指向数组类型的指针时,必须带有圆括号。
C++11新标准,使用 auto 和 decltype 能省略复杂的指针定义。
使用标准库函数 begin 和 end 也能实现同样的功能,而且看起来更简洁一些。
测试用例:
#include
#include
using namespace std;
int main()
{
int ia[3][4]; // 大小为3的数组,每个元素是含有4个整数的数纽
int(*p)[4] = ia; // p指向含有4个整数的数组
p = &ia[2]; // p指向ia的尾元素
/*
// 输出ia中每个元素的值,每个内层数组各占一行
// p指向含有4个整数的数组
for (auto p = ia; p != ia + 3; ++p)
{
// q指向4个整数数组的首元素,也就是说,q指向一个整数
for (auto q = *p; q != *p + 4; ++q)
cout << *q << ' ';
cout << endl;
}
*/
// p指向ia的第一个数组
for (auto p = begin(ia); p != end(ia); ++p)
{
// q指向内层数组的首元素
for (auto q = begin(*p); q != end(*p); ++q)
cout << *q << ' '; // 输出q所指的整数值
cout << endl;
}
system("pause");
return 0;
}