除了内置类型之外,C++ 语言还定义了一个内容丰富的抽象数据库类型。其中 string 和 vector 是最重要的标准库类型。前者支持可变长字符串,后者则表示可变长的集合。还有一种标准库类型是迭代器,它是 string 和 vector 的配套类型,常被用于访问 string 中的字符或 vector 中的元素。内置数组是一种更基础的类型,string 和 vector 都是对它的某种抽象。
内置类型是由 C++ 语言直接定义的,体现了大多数计算机硬件本身具备的能力,标准库定义了另外一组具有更高级性质的类型。它们尚未直接实现到计算机硬件中。
命名空间的 using 声明
作用域操作符(::)的含义是:编译器应从操作符左侧名字所示的作用域中寻找右侧那个名字。还有一种更简单的方法也能使用命名空间中的成员,也就是使用 using 声明。
有了 using 声明,就无须专门的前缀(形如命名空间::)也就能使用所需的名字了。using 声明具有如下形式:
1 | using namespace::name |
一旦声明了上述语句,就可以直接访问命名空间中的名字。
- 每个 using 声明引入命名空间中的一个成员。用到的每个名字都必须有自己的声明语句,而且每条语句都以分号结束
- 一般来说头文件不应该包含 using 声明,这是因为头文件的内容会被拷贝到所有引用它的文件中去,如果头文件里有个 using 声明,那么每个使用了该头文件的文件都会有这个声明,这可能造成始料未及的名字冲突
标准库类型 string
标准库 string 类型表示可变长的字符序列,使用 string 类型必须首先包含 string 头文件。作为标准库的一部分,string 定义在命名空间 std 中。
C++ 标准一方面对库类型所提供的操作做了详细规定,另一方面也对库的实现者作出一些性能上需求。因此标准库类型对于一般应用场合来说有足够的效率。
定义和初始化 string 对象
如何初始化类的对象是由类本身决定的,一个类可以定义很多种初始化对象的方式。只不过这些方式之间必须有所区别:或者是初始值的数量不同,或者是初始值的类型不同。
对于 string 类型,常用的初始化方式
- 默认初始化:得到空字符串
- 通过一个字符串字面值初始化,则该字面值中除了最后那个空字符外其他所有的字符都被拷贝到新创建的 string 对象中去
- 通过数字和字符初始化,string 对象的内容是给定字符连续重复若干次后得到的序列
如果使用等号(=)初始化一个变量,实际上执行的是拷贝初始化(copy initialization),编译器把等号右侧的初始值拷贝到新创建的对象中去,如果不使用等号,则执行的是直接初始化。
当初始值只有一个时,使用直接初始化或拷贝初始化都行,但是如果需要用到多个初始值,一般来说只能用直接初始化的方式。对于用多个值进行初始化的情况,非要用拷贝初始化的方式来处理也不是不可以,此时需要显式地创建一个临时对象用于拷贝。例如
1 | string s = string(10, 'c'); |
string 对象上的操作
一个类除了要规定初始化其对象的方式外,还要定义对象上所能执行的操作。其中,类既能定义通过函数名调用的操作,也能定义 <<
+
等各种运算符在该类对象上新的含义。
- 读写 string 对象:可以使用 IO 操作符读写 string 对象。在执行读取操作时,string 对象会自动忽略开头的空白(空格符、换行符、制表符等)并从第一个真正的字符开始读起,直到遇见下一处空白为止
- getline 函数从给定的输入流中读入内容,直到遇到换行符为止(换行符也被读进来了),所读入的内容被保存到那个 string 对象中去(注意不存换行符),getline 只要一遇到换行符就结束读取操作并返回结果,getline 也返回它的流参数
- empty:string 类型的一个成员函数,判断 string 对象是否为空
- size:string 对象的一个成员函数,返回 string 对象的长度(即 string 对象中字符的个数)。size 函数返回的是一个 string::size_type 类型的值,通过作用域操作符来表明名字 size_type 是在类 string 中定义的
- string 类及其他大多数标准库类型都定义了几种配套的类型,这些配套类型体现了标准库类型与机器无关的特性。
- string::size_type 是一个无符号类型的值,而且能足够存放下任何 string 对象的大小
- 可以通过 auto 或 decltype 让编译器推断变量的类型,这样就不需要显式定义 string::size_type 类型了
- 如果一条表达式中已经有了 size() 函数就不要再使用 int 了,这样就可以避免混用 int 和 unsigned 可能带来的问题
- string 类型定义了几种用于比较字符串的运算符,这些运算符逐一比较 string 对象中的字符并且对大小写敏感(按照字典顺序)
- 为 string 对象赋值:允许将一个 string 对象赋值给另外一个对象
- string 对象相加:两个 string 对象相加得到一个新的 string 对象,其内容是把左侧的运算符对象与右侧的运算对象串接而成
- 字面值和 string 对象相加:标准库允许把字符串字面值和字符字面值转换成 string 对象,因此在需要 string 对象的地方就可以使用这两种字面值来代替,但是此时必须确保每个加法运算符两侧的运算对象至少有一个是 string
- 因为某些历史原因,也为了与 C 兼容,C++ 语言中的字符串字面值并不是标准库类型 string 的对象,切记,字符串字面值与 string 是不同的类型
- cctype 头文件定义了一系列字符处理函数
C++ 标准库除了定义 C++ 语言特性特有的功能之外,也兼容了 C 语言标准库。C 语言的头文件形如 name.h,C++ 则将这些文件命名为 cname(c 表示这是一个属于 C 语言标准库的头文件)。一般来说,cname 和 name.h 头文件的内容是一样的,只不过从命名规范上来讲更符合 C++ 语言的要求,而且在 cname 头文件中定义的名字从属于命名空间 std,而定义在名为 .h 的头文件中则不是。
C++ 程序员应该使用 cname 的头文件而不是 name.h,标准库的名字总是能从命名空间 std 中找到。如果使用 .h 形式头文件,程序员就需要记住哪些是从 C 语言那儿继承过来的,哪些又是 C++ 语言所独有的。
C++ 新标准提供了范围 for 语句。这种语句遍历给定序列中的每个元素,并对序列中的每个值执行某种操作。其语法形式为:
1 | for (declaration:expression) |
其中 expression 部分是一个对象,用于表示一个序列。declaration 部分负责定义一个变量,该变量被用于访问序列中的基础元素。每次迭代,declaration 部分的变量会被初始化为 expression 部分的下一个元素值。
如果想要改变 expression 对象中的元素值,那么必须把循环变量定义成引用类型。这样引用变量依次被绑定到序列上的每一个元素。使用该引用,我们就能改变它绑定对象的值了。
要想访问 string 对象中的单个字符有两种形式:第一种是使用下标,另外一种是使用迭代器。下标运算符([])接收的输入参数是 string::size_type 类型的值,这个参数表示要访问的字符的位置,返回值是该位置上字符的引用。string 对象的下标从 0 开始。不管什么时候只要对 string 对象使用了下标,就要确认那个位置上确实有值。使用超出范围的下标将引发不可预知的结果。
通过下标可以执行迭代操作,利用下标也可以执行随机访问。无论何时用到字符串的下标,都应该注意检查其合法性。
标准库 vector 类型
标准库类型 vector 表示对象的集合,其中所有对象的类型都相同。集合中的每个对象都有一个与之对应的索引,索引用于访问对象。因为 vector 容纳着对象,因此也经常被称为容器。要想使用 vector,需要包含头文件 <vector>
。
C++ 语言既有类模板,也有函数模板。其中 vector 是一个类模板,模板本身不是类或函数,相反可以将模板看做为编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程称为实例化。当使用模板时,需要告诉编译器应当把类模板或函数模板实例化成何种类型。
对于类模板,需要提供一些额外的信息来指定模板到底实例化成什么样的类,需要提供哪些信息由模板决定。提供信息的方式总是这样,即在模板名字后面跟一对尖括号,在括号内放上信息。
对于 vector 来说,需要提供的额外的信息是 vector 内所存放对象的类型。所以 vector 本身是模板而非类型,由 vector 生成的类型必须包含 vector 中元素的类型,如 vector
早期版本 C++ 标准中,如果 vector 的元素还是 vector,则其定义的形式与现在的 C11 新标准略有不同。过去必须在外层 vector 对象的右层尖括号和其元素类型之间添加一个空格,如应该写成 vector<vector<int> >
,而非 vector<vector<int>>
,C11 新标准则没有该限制。
定义和初始化 vector 对象
- 可以默认初始化 vector 对象,从而创建一个指定类型的空 vector
- 也可以在定义 vector 对象时指定元素的初始值
- C++11 新标准还提供了另一种为 vector 对象的元素赋予初值的方法,即列表初始化,用花括号括起来的 0 个或多个初始元素值被赋给 vector 对象。
- 还可以用 vector 对象容纳的元素数量和所有元素的统一初始值来初始化 vector 对象
- 也可以只提供 vector 对象容纳的元素数量并略去初始值,此时库会创建一个值初始化的元素初值,并把它赋给容器中的所有元素,这个初值由 vector 对象中元素的类型决定。如果元素是内置类型,比如 int,其初始值自动设置为 0,如果元素是某种类型,则元素由类默认初始化。这种初始化方式有两个限制:第一,有些类要求必须明确地提供初始值,如果 vector 中元素的类型不支持默认初始化,我们必须提供初始的元素值。第二,如果只提供了元素的数量而没有设定初始值,只能使用直接初始化
- 由于引用不是对象,因此不存在包含引用的 vector
C++ 语言提供了几种初始化形式,在大多数情况下这些初始化形式可以相互等价地使用,但是有一些例外:
- 使用拷贝初始化时(即使用=时),只能提供一个初始值
- 如果提供的是一个类内初始值,只能使用拷贝初始化或花括号的形式初始化
- 如果提供是初始元素的列表,则只能把初始值都放在花括号列表里进行初始化,而不能放在圆括号里
- 如果用的是圆括号,可以说提供的值时用来构造 vector 对象的;如果用的是花括号,可以表述成我们想列表初始化该 vector 对象。也就是说初始化过程会尽可能地把花括号内的值当成是元素初始值的列表来处理,只有在无法执行列表初始化时才会考虑其他初始化方式
- 如果初始化使用了花括号对的形式,但是提供的值又不能用来列表初始化,就要考虑用这样的值来构造 vector 对象了。
向 vector 对象中添加元素
- vector 的成员函数 push_back 可以向 vector 对象添加元素,其会把一个值当成 vector 对象的尾元素压入到 vector 对象的尾端
- vector 对象能高效地增长:C++ 标准要求 vector 应该能在运行时高效快速地添加元素。
- 开始的时候创建空的 vector 对象,在运行时再动态地添加元素,这一做法与 C 语言及大多数其他语言中内置数组类型的用法不同
- 如果循环体内部含有向 vector 对象添加元素的语句,则不能使用范围 for 循环
- 范围 for 语句的循环体内不应改变其所遍历序列的大小
其他 vector 操作
除了 push_back 之外,vector 还提供了几种其他操作,大多数都和 string 的相关操作相似。访问 vector 对象中元素的方法和访问 string 对象中字符的方法差不多。
只用下标运算符能获取到指定的元素。vector 对象的下标也是从 0 开始计算,下标类型是相应的 size_type 类型。
需要注意,在使用 vector 定义的 size_type 类型时,首先需要指出它是由哪种类型定义。vector 对象的类型总是包含着元素的类型,因此 vector<int>::size_type
是正确的写法,而 vector::size_type
是错误的写法。
需要特别注意,不能用下标形式添加元素。vector 对象(以及 string 对象)的下标运算符只能用于访问已经存在的元素,而不能用于添加元素。
只能对明确已存在的元素执行下标操作。确保下标合法的一种有效手段就是尽可能使用范围 for 语句。
迭代器介绍
可以使用下标运算符来访问 string 对象的字符或 vector 对象的元素,但是还有一种更通用的机制也可以实现同样的目的,这就是迭代器。标准库容器都可以使用迭代器,但是只有少数几种才同时支持下标运算符。
类似于指针类型,迭代器提供了对对象的间接访问,就迭代器而言,其对象是容器中的元素或者 string 对象中的字符。
使用迭代器
支持迭代器的类型拥有返回迭代器的成员(方法),例如这些类型都拥有名为 begin 和 end 的成员。begin 成员负责返回指向第一个元素的迭代器,end 成员则负责返回指向容器尾元素的下一位置的迭代器,因此该迭代器只能用于作为一个标记,表示我们已经处理完了容器中的所有元素。
如果容器为空,则 begin 和 end 返回的是同一个迭代器,都是尾后迭代器,或者简称尾迭代器。
一般来说,我们不清楚迭代器的准确类型到底是什么。可以使用 auto 关键字定义迭代器类型。
和指针类似,也能通过解引用迭代器来获取它所指示的元素。执行解引用的迭代器必须合法并确实指向某个元素。试图解引用一个非法迭代器或者尾后迭代器都是未定义的行为。end 返回的迭代器并不实际指示某个元素,所以不能对其进行递增或解引用操作。
迭代器使用递增运算符来从一个元素移动到下一个元素。迭代器的递增是将迭代器向前移动一个位置。
在 for 循环中使用迭代器时,一般习惯使用 !=
而非 <
进行判断。因为这种编程风格在标准库提供的所有容器上都有效。所有的标准库容器的迭代器都定义了 ==
和 !=
,但是它们中的大多数都没有定义 <
运算符。
迭代器类型
一般来说我们不需要知道迭代器的精确类型。实际上那些拥有迭代器的标准库类型使用 iterator
和 const_iterator
来表示迭代器的类型。const_iterator 和常量指针差不多,能读取但不能修改它所指的元素值,而 iterator 的对象可读可写。如果容器对象是一个常量,则只能使用 const_iterator。
being() 和 end() 返回的具体类型由对象是否是常量决定,如果对象是常量,begin() 和 end() 返回 const_iterator,如果对象不是常量,则返回 iterator。
C++11 新标准引入了两个新函数,分别是 cbegin() 和 cend(),不论 vector 对象本身是否是常量,返回值都是 const_iterator。
结合解引用和成员访问操作符
解引用迭代器可获得迭代器所指的对象,如果该对象的类型恰好是类,就有可能希望进一步访问它的成员。为了简化这些操作,C++ 定义了箭头运算符(->),箭头运算符把解引用和成员访问两个操作结合在一起。也就是说 it->mem 和 (*it).mem 表达的意思相同。
某些对 vector 对象的操作会使迭代器失效
虽然 vector 对象可以动态地增长,但是也会有一些副作用。之前讲过不能在范围 for 循环中向 vector 对象添加元素。另外一个限制是:任何一种可能改变 vector 对象容量的操作,比如 push_back,都会使该 vector 对象的迭代器失效。
因此需要牢记,但凡是使用了迭代器的循环,都不要像迭代器所属的容器添加/删除元素。
迭代器运算
所有的标准库容器的迭代器都支持递增运算,也能用 == 和 != 对任意标准库类型的两个有效迭代器进行比较。
string 和 vector 的迭代器提供了更多额外的运算符,一方面可以使得迭代器每次移动多个元素,另外也支持迭代器关系运算。可以让迭代器和一个整数值相加(或相减),其返回值是向前(或向后)移动了若干个位置的迭代器。除了判断是否相等,还能使用关系运算符来对其进行比较。参与的两个迭代器必须合法而且指向的是同一个容器的元素。只要两个迭代器指向的是同一个容器中的元素,就能执行相减运算,所得结果是两个迭代器的距离,其类型名为 difference_type 的带符号整数。
数组
数组是一种类似于标准库类型 vector 的数据结构,但是在性能和灵活性的权衡上又与 vector 有所不同。
- 与 vector 相似的是,数组也是存放类型相同的对象的容器,这些对象本身没有名字,需要通过其所在位置访问。
- 与 vector 不同的是,数组的大小确定不变,不能随意向数组中增加元素,因为数组的大小固定。因此对某些应用来说程序的运行时性能较好,但是相应地也损失了一些灵活性
- 如果不清楚元素的确切个数,请使用 vector
数组是一种复合类型,数组的声明形式为 a[d],其中 a 是数组的名字,d 是数组的维度。数组中元素的个数也属于数组类型的一部分,编译的时候维度应该是已知的。也就是说,维度必须是一个常量表达式。
定义数组的时候必须指定数组的类型,不能使用 auto 关键字由初始值的列表推断类型。另外和 vector 一样,数组的元素应为对象,因此不存在引用的数组。
可以对数组的元素进行列表初始化,此时允许忽略数组的维度。如果在声明时如果没有指明维度,编译器会根据初始值的数量计算并推测出来。如果指明了维度,那么初始值的总数量不应该超过指定大小。如果初始值的数量小于维度,则用提供的初始值初始化靠前的元素,剩下的元素被初始化为默认值。字符数组有一种额外的初始化形式,可以用字面串字面值对此类数组初始化,此时一定要注意字符串字面值的结尾处还有一个空字符,该空字符也会被拷贝到字符数组中。
不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值。
数组本身就是对象,所以允许定义数组的指针以及数组的引用。
- int *ptr[10]:ptr 是一个含有 10 个元素的数组,元素的类型为指向 int 的指针
- int &ref[10]:错误,不存在引用数组
- int (*Parray)[10] = &array:Parray 是一个指针,指向含有 10 个 int 的数组
- int (&Rarray)[10] = array:Rarray 是一个引用,引用一个含有 10 个 int 的数组
要想理解数组声明的含义,最好的办法就是从数组的名字开始按照由内向外的顺序阅读。
与标准库类型 vector 和 string 一样,数组的元素也能使用范围 for 语句或使用下标运算符来访问。在使用数组下标的时候,通常将其定义为 size_t 类型。在 cstddef 头文件中定义了该类型,该文件是 C 标准库 stddef.h 头文件的 C++ 语言版本。
数组除了大小固定这一特点之外,其他用法和 vector 基本类似。
指针和数组
- 在 C++ 语言中,指针和数组有非常紧密的联系。使用数组的时候,编译器一般会把它转换为指针。
- 可以像其他对象一样,对数组的元素使用取地址符就能得到指向该元素的指针。
- 在很多用到数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指针
- 当使用数组作为一个 auto 变量的初始值时,推断得到的类型是指针而非数组
- 但是当使用 decltype 关键字时,该转化过程不会发生,此时返回的是一个数组类型(元素的个数也是数组类型的一部分)
- 指针也是迭代器,指向数组元素的指针拥有更多功能。vector 和 string 迭代器支持的运算,数组的指针全部支持
- 尽管能计算得到尾后指针,但这种做法极容易出错。为了让指针的使用更简单安全,C++ 11 新标准引入了两个名为 begin 和 end 的函数。这两个函数与容器中的两个同名成员功能类似,但是数组不是类类型,因此这两个函数不是成员函数,正确的使用方法是将数组作为它们的参数。这两个函数定义在 iterator 头文件中
- 指向数组元素的指针可以执行解引用、递增、比较、与整数相加、两个指针相减等运算,用在指针和用在迭代器上意义完全一致。两个指针相减的结果的类型是一种名为 ptrdiff_t 的标准库类型,定义在头文件 cstddef 中,它是一种带符号类型。
- 对数组执行下标运算其实就是对指向数组元素的指针执行下标运算,只要指针指向的是数组中的元素,都可以执行下标运算。标准库类型限定使用的下标必须是无符号类型,而内置的下标运算则无此要求。内置的下标运算符所引用的索引值不是无符号类型,这一点与vector 和 string 不一样
C 风格字符串
- 字符串字面值是一种通用结构的实例,这种结构即是 C++ 由 C 继承而来的 C 风格字符串。C 风格字符串不是一种类型,而是为了表达和使用字符串而形成的一种约定俗称的写法。按此习惯书写的字符串存放在字符数组中并以空字符结束。
- C 标准库提供了一组函数,用于操纵 C 风格字符串,它们定义在 cstring 头文件中。cstring 是 C 语言头文件 string.h 的 C++ 版本。注意,传入此类函数的指针必须指向以空字符作为结束的数组
- 标准库 string 对象使用普通的关系运算符和相等性运算符。但是如果把这些运算符作用在两个 C 风格字符串,实际上比较的将是指针而非字符串本身,要想比较两个 C 风格字符串,需要调用 strcmp 函数
- 连接或拷贝 C 风格字符串也与标准库 string 对象的同类操作差别很大,对于 C 风格字符串,需要使用 strcat 和 strcpy 函数,使用这两个函数需要提供一个存放结果字符串的数组,该数组必须足够大以便容纳结果字符串(包括末尾的空字符)
- 对于大多数应用来说,使用标准库 string 要比使用 C 风格字符串更安全、更高效
与旧代码的接口
有时候,现代的 C++ 程序不得不与那些充满了数组/C 风格字符串的代码衔接,为了使这一工作简单易行,C++ 专门提供了一组功能:
- 混用 string 对象和 C 风格字符串:允许以空字符结束的字符数组来初始化 string 对象或为 string 对象赋值,在 string 对象的加法运算中允许使用以空字符结束的字符数组作为其中一个运算对象
- 但是如果程序的某处需要一个 C 风格字符串,无法直接使用 string 对象来代替它。string 专门提供了一个名为 c_str 的成员函数,它返回一个 C 风格字符串(即返回一个指针,该指针指向一个以空字符结束的字符数组,该数组保存的数据恰好就是 string 对象的内容),结果指针的类型是 const char *,无法保证 c_str 函数返回的数组一直有效,如果后续操作改变了 string 的值,则之前返回的数组可能失去效用
- 如果执行完 c_str 函数之后,程序想一直能使用其返回的数组,最好将该数组重新拷贝一份
- 允许使用数组来初始化 vector 对象,要实现这一目的,只需要指明要拷贝区域的首元素地址和尾后地址就可以了
现代 C++ 程序应当尽量使用 vector 和迭代器,避免使用内置数组和指针。应该尽量使用 string,避免使用 C 风格的基于数组的字符串。
多维数组
多维数组其实就是数组的数组。当一个数组的元素仍然是数组时,通常使用两个维度来定义它:一个维度表示数组的大小,另一个维度表示其元素(也是数组)的大小。对于二维数组来说,常把第一个维度称作行,第二个维度称作列。
- 允许使用花括号括起来的一组值初始化多维数组,多维数组的每一行分别用花括号括起来,但是内层嵌套着的花括号并非必须的;
- 类似于一维数组,在初始化多维数组时也并非所有元素的值都必须包含在初始化列表中
- 可以使用下标运算符来访问多维数组的元素,此时数组中的每一个维度对应一个下标运算符
- 如果表达式含有的下标运算符数量和数组的维度一样多,该表达式的结果将是给定类型的元素;反之,如果表达式含有的下标运算符数量比数组的维度小,则表达式的结果将是给定索引处的一个内层数组
- 可以使用范围 for 语句处理多维数组
- 要使用范围 for 语句处理多维数组,除了最内层的循环之外,其他所有循环的控制变量都应该是引用类型,否则编译器初始化时会自动将这些数组形式的元素(和其他类型的数组一样)转换成指向该数组内首元素的指针
- 当程序使用多维数组的名字时,也会自动将其转换为指向数组首元素的指针,也就是指向第一个内层数组的指针
- 随着 C++11 新标准的提出,通过使用 auto 或者 decltype 就能尽可能地避免在每个数组前面加上一个指针类型了
- 读、写、理解一个多维数组的指针是一个很容易出错的工作,使用类型别名能让这项工作变得简单一点