像数字、字符这样的内置类型,体现了大多数计算机硬件本身具备的能力。C++ 还通过标准库提供了一组具有更高级性质的类型,它们尚未在计算机硬件中直接实现,比如:表示可变长字符序列的 string
、表示某种给定类型对象的可变长序列的 vector
。而内置数组类型则与其它内置类型一样,其实现和硬件密切相关,因此灵活性上稍显不足。
命名空间的 using
声明
作用域操作符 ::
的含义是:编译器应从操作符左侧名字所指的作用域中寻找右侧的名字。使用 ::
访问命名空间中的成员较为繁琐,通过 using
声明,无需前缀,就可以访问到所需的名字。
cpp
using namespace::name;
每条 using
声明引入命名空间中的一个成员,未引入的成员仍需 ::
访问。
cpp
#include <iostream>
using std::cin;
int main()
{
int i;
cin >> i;
std::cout << i;
return 0;
}
由于头文件内容会拷贝到所有 #include
它的文件中,因此头文件中的 using
可能会导致预料不到的命名冲突,所以头文件中不应使用 using
声明。
标准库类型 string
标准库类型 string
表示可变长的字符序列。
C++ 标准一方面详细规定了库类型所提供的操作,另一方面也对库的实现者提出了性能上的要求。
定义和初始化 string
对象
如何初始化类的对象由类本身决定,一个类可以有多种初始化方式,不同方式之间有所区别:初始值的数量不同或者初始值的类型不同。最常用的初始化 string
对象的方式如下:
有多种不同的初始化方式:
- 拷贝初始化 (copy initialization),使用
=
初始化变量,编译器将=
右侧的初始值拷贝到新建对象上去; - 直接初始化 (direct initialization),使用
()
而非=
来初始化变量。
初始化参数只有一个时,直接初始化和拷贝初始化都行。初始化参数有多个时,一般使用直接初始化,若要使用拷贝初始化,则需显式创建临时对象用于拷贝。
cpp
string s = string(10, 'c');
// 等价于下面两条语句
string temp(10, 'c');
string s = temp;
string
对象上的操作
除了初始化之外,类还需要定义对象上能执行的操作,类既能定义通过函数名调用的操作,也能定义运算符(<<
、+
等)对该类对象的作用。下表罗列了 string
对象上可以进行的大多数操作。
读写
使用标准库中的 iostream
,并借助 IO 操作符可以读写 string
对象。执行读取操作时,string
对象会自动忽略开头的空白(即空格符、换行符、制表符等),从第一个真正的字符开始,直到下一处空白结束。string
对象的输入输出操作返回的也是运算符左侧的运算对象。
getline
使用 getline
可以读取一整行字符------包括空白符,函数将从给定的输入流中读取内容,直到遇到换行符为止------换行符也读取,然后将所读内容------不包括换行符------存入 string
对象。
empty
、size
和 string::size_type
empty
和 size
都是 string
对象上的成员函数。size
函数返回的是一个 string::size_type
类型的值。string
类和其它大多数标准库类型都定义了几种配套类型,这些配套类型体现了标准库类型与机器无关的特性,类型 size_type
就是其中之一。string::size_type
类型是一种无符号整型,而且能够放得下任何 string
对象的大小。可以使用 auto
或者 decltype
推断 string::size_type
类型。
cpp
auto len = line.size();
表达式中已经有
size()
函数就不要再使用int
,这样可以避免int
和unsigned
混用所带来的问题。
比较
比较运算符 ==
、!=
、<
、<=
、>
、>=
会逐一比较 string
对象中的字符,而且对大小写敏感。string
对象的大于小于则按照字典序比较:
- 如果两个
string
对象在某些对应位置上不一致,则string
对象的比较结果就是第一对相异字符比较的结果。 - 如果两个
string
对象在相应位置上的字符都相同,则较长的对象大于较短的对象。
设计标准库类型时都力求在易用性上向内置类型看齐,因此大多数库类型都支持赋值操作。string
对象之间可以互相赋值,此时是对 string
对象的内容进行拷贝。
加法
string
对象相加就是按顺序将两者的内容串起来,并返回一个新的 string
对象。
标准库允许把字符字面量和字符串字面量转换为 string
对象。在某些需要 string
对象的地方可以用这两种字面量代替。当 string
对象和字符串字面量混在一起相加时,必须保证每个 +
两侧的运算对象中至少有一个是 string
对象。
字符串字面量不是标准库中的
string
类型。
处理 string
对象中的字符
单独处理 string
对象中的字符涉及到两个关键问题:
- 如何获取字符本身;
- 如何处理某个字符。
cctype
头文件中定义了一组处理字符的标准库函数,下表列出了其中主要的函数名。
除了 C++ 语言特有功能外,C++ 标准库还兼容了 C 语言的标准库。C 语言中形如
name.h
的头文件,在 C++ 中命名为cname
。比如,cctype
头文件和ctype.h
内容一样。名为cname
的头文件中定义的名字从属于命名空间std
,而name.h
则不然。一般 C++ 程序应使用名为cname
的头文件而不是name.h
。
范围 for
(range for)语句遍历序列中的每个元素并对它们执行某种操作:
cpp
for (declaration: expression)
statement
expression
是一个表示序列的对象,declaration
定义一个用于访问序列中基础元素的变量,每次迭代,该变量会被初始化为 expression
中的下一个元素值。declaration
也可以定义为元素值的引用,从而修改序列对象 expression
中的元素。
cpp
string s = "abc";
for (auto &c: s) {
c = 'X';
}
访问 string
对象中的字符有两种方式:
- 使用下标,或称为索引;
- 使用迭代器。
下标运算符 []
的输入参数是 string::size_type
类型的值,表示要访问的字符位置,返回的是该位置上字符的引用。只要表达式的值是一个整型数就可以作为索引,有符号型会自动转换为 string::size_type
类型。只要 string
对象不是常量,就可以通过下标运算符赋值。
string
对象的下标必须大于等于 0 而小于 s.size()
,C++ 标准并不要求标准库检测下标的合法性,使用超出范围的下标将引发不可预知的后果,因此使用下标前必须先确定该位置上有值。设置下标为 string::size_type
类型可以保证一定不小于 0,此时只需检查是否小于 size()
的值即可。
逻辑与运算符
&&
只在左侧运算对象为真时,才会检查右侧运算对象。
标准库类型 vector
标准库类型 vector
表示对象的集合,这些对象的类型都相同,每个对象都有一个索引。因为 vector
"容纳着"其它对象,所以也常被称为容器(container)。
C++ 语言既有类模板 (class template),也有函数模板,vector
是一个类模板。模板本身不是类或函数,模板可以看作是为了让编译器生成类或函数而编写的一份说明。编译器根据模板创建类或函数的过程称为实例化(instantiation),使用模板时,需要提供一些信息,告诉编译器应把类或函数实例化为哪种类型。
vector
是模板而非类型,vector
生成的类型必须包含 vector
中元素的类型,比如:vector<int>
。除了引用之外,绝大多数内置类型和类类型都可以构成 vector
对象,甚至组成 vector
的元素也可以是 vector
。
某些编译器可能仍需以老式声明语句处理元素为
vector
的vector
对象,如:vector<vector<int> >
,即>
和元素类型之间有一个空格。
定义和初始化 vector
对象
vector
模板控制着定义和初始化向量的方法,下表是常用的定义 vector
对象的方法。
- 使用初始值列表对
vector
对象列表初始化时,必须用{}
,而不能用()
。 - 只提供
vector
对象容纳的元素数量时,库会创建一个值初始化的(value-initialized)元素初值,并赋给容器中的所有元素,这要求元素类型可以默认初始化。 - 有时,初始化的真实含义依赖于传递初始值时用的是
()
还是{}
。使用()
是通过提供的值构造vector
对象。使用{}
则首先尝试对vector
对象列表初始化,如果列表初始化失败,则会尝试构造vector
对象。
cpp
vector<string> svec{10}; // 等价于 vector<string> svec(10)
vector<string> svec1{10, "hi"}; // 等价于 vector<string> svec1(10, "hi")
向 vector
对象中添加元素
vector
的成员函数 push_back
可以在 vector
对象尾部添加一个元素。
C++ 标准要求 vector
能在运行时高效快速地添加元素,这使得定义 vector
对象时设定大小变得没有必要,除非所有元素的值都一样。因此,通常都是初始化一个空的 vector
对象,运行时再添加元素。
向 vector
对象添加元素蕴含了编程假定:必须确保所写的循环正确无误,特别是在循环可能改变 vector
对象容量时;如果循环体内向 vector
对象添加元素,则不能使用范围 for
循环。
范围
for
语句体内不应改变所遍历的序列大小。
其它 vector
操作
下表列出了 vector
对象比较重要的几种操作。
- 范围
for
循环也可以处理vector
对象中的元素。 vector<T>
对象的size
方法返回值类型为vector<T>::size_type
。- 只有当元素的值可比较时,
vector
对象才能比较。 - 只要
vector
对象不是常量,就能向下标运算符返回的元素赋值。 vector
对象(以及string
对象)的下标运算符可用于读写元素,但不能用于添加元素。
整数相除得到的是商。
访问元素时,如果下标越界,会产生不可预知的问题,编译器不会检查这种错误。缓冲区溢出(buffer overflow)就是这种错误,这也是 PC 及其他设备上应用程序产生安全问题的一个重要原因。
确保下标合法的一种有效手段是尽可能使用范围
for
语句。
迭代器介绍
除了 vector
之外,标准库还定义了其它几种容器,它们都可以使用迭代器 (iterator),但只有少数几种可以使用下标运算符。string
不是容器,但也可以使用迭代器。
和指针类似,迭代器可以间接访问对象。迭代器的对象是容器中的元素或 string
中的字符,使用迭代器可以访问某个元素,也能在元素间移动。有效的迭代器指向某个元素,或容器中尾元素的下一个位置,其它情况均无效。
使用迭代器
拥有迭代器的类型都会有相应的返回迭代器的成员,比如:begin
、end
成员。begin
成员返回指向第一个元素/字符的迭代器。end
成员返回指向尾后 (off the end)------尾元素下一个位置------的迭代器,也称为尾后迭代器 (off-the-end iterator)或尾迭代器(end iterator)。尾迭代器标记着已经处理完容器中所有元素。
如果容器为空,则
begin
和end
返回的是同一个迭代器。
下表列出了迭代器支持的一些运算。
- 迭代器可以通过解引用符
*
来获取所指元素的引用,执行解引用的迭代器必须是指向了某个元素的有效迭代器,解引用无效迭代器或尾迭代器的后果都是不可预知的。 - 迭代器可以使用递增运算符
++
移动到下一个元素。尾迭代器不能使用递增操作。移动迭代器越界将引发错误。 - 箭头运算符
->
是解引用和成员访问两个操作的结合,比如it->mem
等价于(*it).mem
。
所有标准库容器都定义了迭代器以及迭代器的
!=
、==
比较运算,但大多数没有定义下标运算或迭代器的<
比较运算,因此最好使用迭代器和!=
、==
,这样的代码无关容器类型。
那些拥有迭代器的标准库类型使用 iterator
和 const_iterator
来表示迭代器类型。const_iterator
类似于指向常量的指针,对所指元素只读;iterator
对所指元素可读可写。常量容器/string
只能使用 const_iterator
,非常量则可以使用 iterator
和 const_iterator
。
begin
和 end
返回的迭代器类型依赖于对象是否为常量,常量对象返回 const_iterator
类型,非常量对象返回 iterator
类型。cbegin
和 cend
成员返回的一定是 const_iterator
类型,不管对象是否为常量。
vector
对象具有如下限制:任何一种可能改变 vector
对象容量的操作,比如 push_back
,都会使该 vector
对象的迭代器失效。
使用了迭代器的循环体,不能再向迭代器所属的容器中添加元素。
迭代器运算
除了所有标准库容器都支持的 ++
、!=
、==
之外,string
和 vector
的迭代器还提供了更多额外的运算符,所有这些运算被称为迭代器运算(iterator arithmetic)。
迭代器的这些算术和比较运算都是针对所指的位置。同一个容器的两个迭代器之差返回的是类型为 difference_type
的有符号整数,string
和 vector
上面都定义了 difference_type
。
数组
数组也是用于存放类型相同的对象的容器,它的大小固定不变。
定义和初始化内置数组
数组是一种复合类型,元素个数是数组类型的一部分,必须大于 0,而且编译时需要知道,因此是一个常量表达式。定义数组时必须明确指出数组元素的类型。
- 默认情况下,数组元素被默认初始化,数组中元素的值不确定。
- 数组元素必须是对象,而不能是引用。
cpp
int arr[10];
数组可以列表初始化。
- 若未指明元素个数,编译器将根据初始值自动推算;
- 初始值数量不能超过元素个数;
- 若初始值数量少于元素个数,则后面的元素将被初始化为默认值。
cpp
int ia1[] = {1, 2}; // int ia1[2] = {1, 2};
int ia2[4] = {1, 2}; // int ia2[4] = {1, 2, 0, 0};
字符数组可以使用字符串字面量初始化,此时结尾的空字符也会计算在内。
cpp
char ca1[] = "hello"; // char ca1[6] = "hello";
const char ca2[] = "ss"; // const char ca2[3] = "ss";
数组不能赋值给其它数组,或初始化其它数组。
有些编译器通过编译器扩展支持数组赋值,但应避免使用非标准特性,因为非标准特性在其他编译器上可能无法工作。
还可以定义复合类型的数组和数组的复合类型。
cpp
int *iPtrArr[10]; // 整型指针数组
int (*iArrPtr)[10] = &arr; // 指向整型数组的指针
int (&iArrRef)[10] = arr; // 整型数组的引用
int *(&iPtrArrRef)[10] = iPtrArr; // 整型指针数组的引用
访问数组元素
数组可以使用范围 for
或下标运算符来访问。数组下标是 size_t
类型,是一种机器相关的无符号类型,它足够大以便能表示内存中任意对象的大小。cstddef
头文件中定义了 size_t
类型。
数组下标应该大于等于 0 而且小于数组大小,这一检查由程序员负责。
指针与数组
数组有一个特性:在很多用到数组名字的地方,编译器都会自动地将其替换为指向数组首元素的指针,比如:auto
推断时。但数组的 decltype
返回值仍是数组。
整个数组作为一个对象,只允许如下操作:
- 取地址(不是赋给指针);
- 绑定到引用;
- 被范围
for
遍历; - 传给
decltype
。
其余所有操作都会自动转换为数组首元素的指针。
指针中存放的是某种类型的对象的地址。指针是所存放值以及相关操作的封装。
cpp
string a1[] = {"one", "two"};
string *p1 = a1; // string *p1 = &a1[0];
auto p2(a1); // string *p2(&a1[0]);
decltype(a1) a2; // string a2[2];
// pa1 和 ps 所存地址相同,但类型不同,指向的对象也不同
auto *pa1 = &a1; // string (*pa1)[2] = &a1;
auto &ra1 = a1; // string (&ra1)[2] = a1;
auto ps = a1; // string *ps = &a1[0];
通过获取数组尾元素之后的那个不存在的元素的地址,所得到的指针称为尾后指针 。这种获取尾后指针的方式极易出错,iterator
头文件中引入了两个函数 begin
、end
,它们与容器的同名成员功能类似。
尾后指针不指向具体元素,也不能对其执行解引用或递增操作。
cpp
int arr[] = {0, 1, 2};
int *s = arr;
++s; // 指向 arr[1]
int *e = &arr[3]; // 尾后指针
auto pbeg = begin(arr);
auto pend = end(arr);
string
和 vector
的迭代器支持的运算,指向数组元素的指针都支持,包括:解引用 *
、递增 ++
、递减 --
、比较 !=
/==
/<
/<=
/>
/>=
、与整数相加减 +n
/-n
、指针相减等,而且与用在迭代器上的意义完全一致。
- 迭代器运算越界时,将会产生错误,这种错误编译器一般发现不了。
- 指向同一数组中元素的指针相减的结果是
ptrdiff_t
类型,它是定义在头文件cstddef
中与机器相关的有符号类型。 - 指向不相关对象的指针不能比较。
- 上述指针运算也适用于空指针和所指对象非数组的指针。对于后者,两个指针必须指向同一个对象或该对象的下一个位置;对于前者,允许给空指针加上或减去一个值为 0 的整型常量表达式;两个空指针相减结果为 0。
cpp
constexpr size_t sz = 3;
int iarr[sz] = {1, 2, 3};
auto e = iarr + sz;
int i = *(iarr + 2);
与 string
和 vector
不同的是,内置数组的下标运算符允许对任何指向数组元素的指针进行有符号数索引。
cpp
int ia[] = {0, 2, 4, 6};
int i = ia[2]; // int i = *(ia + 2);
auto p = ia + 3;
int i1 = p[-1]; // int i1 = *(p - 1);
C 风格字符串
C 风格字符串 不是一种类型,而是一种为表达和使用字符串而约定的一种写法。符合这种约定的字符串存放在字符数组中,并以空字符结束。通常通过指针来操作这些字符串。
C 风格字符串使用起来不方便,而且极易引发程序漏洞,是很多安全问题的根本原因。C++ 支持 C 风格字符串,但最好不要使用。
C 标准库提供了一组用于操作 C 风格字符串的函数,它们定义在头文件 cstring
中。
这些函数不负责验证其字符串参数,传入这些函数的指针必须指向以空字符结束的数组,否则将产生不确定的结果,函数内部有可能在内存中不断向前寻找,直到遇到空字符才停下来。
由于变量名是指向首字符的指针,C 风格字符串之间需要通过 strcmp
函数来比较,而不能直接用变量名。连接和拷贝 C 风格字符串需要使用 strcat
、strcpy
函数,而且用于存放结果的第一个参数必须足够大以便容纳结果字符串和末尾的空字符。
对于大多数应用来说,标准库
string
比 C 风格字符串更安全、高效。
与旧代码的接口
现代 C++ 程序不得不与那些充满了数组和 C 风格字符串的代码衔接,为了使这一工作简单易行,C++ 专门提供了一组功能。
string
对象和 C 风格字符串混用
C 风格字符串可以参与 string
对象的运算,任何出现字符串字面量的地方都可以使用以空字符结束的字符数组来代替:
- 为
string
对象初始化或赋值。 - 参与
string
对象的加法运算。
但是,string
对象无法直接参与 C 风格字符串的运算中。为了让 string
对象初始化为指向字符的指针,string
提供了成员函数 c_str
,返回一个 C 风格字符串,即一个类型为 const char*
的指向一个以空字符结束的字符数组的指针,该数组所存数据与 string
对象的一样。如果后续操作改变了 string
对象的值,之前返回的数组可能会失效。
cpp
string s("Hello World");
const char *str = s.c_str();
使用数组初始化 vector
对象
通过指定拷贝区域的首元素地址和尾后地址可以用数组来初始化 vector
对象。
cpp
int int_arr[] = {0, 1, 2, 3, 4, 5};
vector<int> sub_vec(int_arr + 1, int_arr + 3);
使用指针和数组容易出错,比如:概念错误、语法错误。现代 C++ 程序应尽可能使用
vector
和迭代器,避免使用内置数组和指针;应尽量使用string
,避免使用 C 风格字符串。
多维数组
严格意义上,C++ 中没有多维数组,多维数组其实是数组的数组。
cpp
int ia[3][4];
int arr[10][20][30];
对于二维数组,常把第一个维度称为行,第二个维度称为列。
多维数组同样可以使用列表初始化,可以将每一行括起来,此时内层 {}
对应每一行;也可以不括,此时从第一行开始逐行排列。和一维数组一样,未提供值的元素初始化为默认值。
cpp
int ia[2][3] = {
{0, 1, 2},
{3},
};
int ia1[2][3] = {0, 1, 2, 3};
可以使用 []
访问多维数组中的元素,数组的每个维度对应一个 []
。
- 若
[]
个数与数组维度相等,则返回给定类型的元素; - 若
[]
个数小于数组维度,则返回给定索引处的一个内层数组。
cpp
int (&row)[3] = ia[1];
多维数组也可以使用范围 for
循环,但要通过引用接收内层数组,避免自动转为指针。
cpp
for (const auto &row: arr) {
for (auto col: row) {
cout << col << endl;
}
}
多维数组的名字也会自动转换为首元素的指针,此时的首元素是一个内层数组。
cpp
int ia[3][4];
auto p = ia; // int (*p)[4] = ia;
p = &ia[2];
还可以为数组类型起一个便于理解的别名。
cpp
using int_array = int[4];
using int_array_ref = int (&)[4];
// 等价于
typedef int int_array[4];
typedef int (&int_array_ref)[4];