C++ Primer - string, vector, array

《C++ Primer》第五版引入了11标准相关内容,我早年在初学C++时还只有第四版,近来想对C++做一个整体的复习+版本升级,借此机会过一遍第五版。本文是阅读第三章“string, vector, array”时所做的笔记。

string, vector, array

命名空间的using声明

使用库函数时都需要加上它的命名空间前缀,如std::cin

显然如果对于所有使用了某库函数的地方都需要这样写的话,势必导致语法又臭又长。C++提供了一种简化方式——使用using。

1
using namespace::name;

比如写

1
2
using std::cin;
using std::cout;

在同一文件中,就可以直接使用cincout而无需命名空间前缀。这些语句不应出现在头文件,当工程较大引用较多时可能会引起名字冲突。

这种办法是最为推荐的,不建议直接using namespace xxx;

头文件中通常不应包含using声明,而应在源文件中独立使用时引入using声明。

string

C++标准库定义的类型,使用起来比从C继承来的char[]在语法上要舒服一些,但实际算法操作细节处理上往往感觉四处碰壁。使用需包含<string>

string并非基本类型,它是标准库内置的一种复合类型,所以定义、初始化、赋值等操作不能按狭义的基本类型理解,它所应用的操作符亦是如此。刷到class时自然懂,所以这里先绕过遮遮掩掩的描述。

###定义和初始化string对象

1
2
3
4
5
string s1;			//默认初始化,s1是一个空串
string s2(s1); //s2是s1的副本
string s2 = s1; //等价于s2(s1),s2是s1的副本
string s3("value"); //s3是字面值"value"的副本,除了字面值最后的那个空字符外
string s4(n, 'c'); //把s4初始化为由连续n个字符c组成的串

###string对象上的操作

支持一堆操作,耳熟能详。

- -
os << s 将s写到输出流os当中,返回os
is >> s 从is中读取字符串赋给s,字符串以空白分割,返回is
getline(is, s) 从is中读取一行赋给s,返回is
s.empty() s为空返回true,否则返回false
s.size() 返回s中字符的个数
s[n] 返回s中第n个字符的引用,位置n从0计起
s1 + s2 返回s1和s2连接后的结果
s1 = s2 用s2的副本代替s1中原来的字符
s1 == s2, s1 != s2 如果s1和s2中所含的字符完全一样,则它们相等;string对象的相等性判断在对字母的大小写敏感
<, <=, >, >= 利用字符在字典中的顺序进行比较,且对字母的大小写敏感

为了与C兼容,C++语言中的字符串字面值并不是标准库string的对象。

处理string对象中的字符

头文件cctype定义了一组标准库函数单独处理string中的字符。

- -
isalnum(c) 当c是字母或数字时为真
isalpha(c) 当c是字母时为真
iscntrl(c) 当c是控制字符时为真
isdigit(c) 当c是数字时为真
isgraph(c) 当c不是空格但可以打印时为真
islower(c) 当c是小写字母时为真
isprint(c) 当c是可打印字符时为真(即c是空格或c具有可视形式)
ispunct(c) 当c是标点符号时为真(即c不是控制字符、数字、字母、可打印空白中的一种)
isspace(c) 当c是空白时为真(即c是空格、横向制表符、纵向制表符、回车符、换行符、进纸符中的一种)
isupper(c) 当c是大写字母时为真
isxdigit(c) 当c是十六进制数字时为真
tolower(c) 如果c是大写字母,输出对应的小写字母;否则原样输出c
toupper(c) 如果c是小写字母,输出对应的大写字母;否则原样输出c

vector

C++标准库定义的类型,是个对象的集合。初学者可以理解成高级的动态数组。顺序容器的一种,实际上是标准库STL设计的类模板,对于不理解模板语法的初学者在使用容器时会感到困惑。无需困惑,学到模板、泛型,自然懂。

因为引用不是对象,所以不存在包含引用的vector。

使用时包含<vector>

类模板实例化之后依然是个类类型,所以去掉最上层包装,它的定义、初始化和string这种类类型没什么区别。

C++11标准引入的列表初始化语法对类类型也适用,只是在一些有歧义的语法冲突时,会放弃该种列表初始化语法,例如:

1
2
3
vector<string> v1{"a","an","the"};		//列表初始化
vector<string> v1 = {"a","an","the"}; //列表初始化
vector<string> v2("a","an","the"); //错误,跟构造函数语法有歧义

在早期的C++标准中,如果vector的元素还是vector,定义时必须在外层vector对象的右尖括号和其元素类型之间添加一个空格,如vector\<vector\<int> >。但是在C++11标准中,可以直接写成vector\<vector\<int>>,不需要添加空格。

定义和初始化vector对象

1
2
3
4
5
6
7
vector<T> v1;				//v1是一个空vector,它潜在的元素是T类型的,执行默认初始化
vector<T> v2(v1); //v2中包含v1所有元素的副本
vector<T> v2 = v1; //等价于v2(v1),v2中包含由v1所有元素的副本
vector<T> v3(n, val); //v3包含了n个重复的元素,每个元素值都是val
vector<T> v4(n); //v4包含了那个重复执行了值初始化的对象
vector<T> v5{a,b,c...}; //v5包含了初始值个数的元素,每个元素被赋予相应的初始值
vector<T> v5={a,b,c...}; //等价于v5{a,b,c...};

vector相关操作

同样支持一堆操作,耳熟能详。

- -
v.empty() 如果v不含有任何元素,返回真;否则返回假
v.size() 返回v中元素的个数
v.push_back(t) 向v的尾端添加一个值为t的元素
v[n] 返回v中第n个位置上元素的引用
v1 = v2 用v2中元素的拷贝替换v1中的元素
v1 = {a, b, c…} 用列表中元素的拷贝替换v1中的元素
v1 == v2 v1和v2相等当且仅当它们的元素数量相同且对应位置的元素值都相同
<, <=, >, >= 顾名思义,以字典顺序进行比较

注意不能用下标形式添加元素。

迭代器

每种可迭代的类型对象都可以有自己的iterator,用于遍历访问,实际上对iterator的支持与否取决于具体类型的实现。初学者可以简单的理解成被封装的指针,目的都是间接访问。iterator本身是个抽象的概念,它按继承关系分成5类,每一种class的iterator都是一种类型。

定义了迭代器的类型都拥有beginend两个成员函数。begin函数返回指向第一个元素的迭代器,end函数返回指向容器“尾元素的下一位置(one past the end)”的迭代器,通常被称作尾后迭代器(off-the-end iterator)或者简称为尾迭代器(end iterator)。尾后迭代器仅是个标记,表示程序已经处理完了容器中的所有元素。

标准容器迭代器的运算符:

- -
*iter 返回迭代器iter所指元素的引用
iter->mem 解引用iter并获取该元素的名为mem的成员,等价于(*iter).mem
++iter 令iter指示容器中的下一个元素
–iter 令iter指示容器中的上一个元素
iter1 == iter2 iter1!= iter2 判断两个迭代器是否相等(不相等),如果两个迭代器指示的是同一个元素或者他们是同一个容器的尾后迭代器,则相等;反之,不相等

在for或者其他循环语句的判断条件中,最好使用!=而不是<。所有标准库容器的迭代器都定义了==!=,但是只有其中少数同时定义了<运算符。

如果vector或string对象是常量,则只能使用const_iterator迭代器,该迭代器只能读元素,不能写元素。begin和end返回的迭代器具体类型由对象是否是常量决定,如果对象是常量,则返回const_iterator;如果对象不是常量,则返回iterator。

1
2
3
4
vector<int> v;
const vector<int> cv;
auto it1 = v.begin(); // it1类型为vector<int>::iterator
auto it2 = cv.begin(); // it2类型为vector<int>::const_iterator

C++11中新增了cbegin和cend,无论vector或string对象是否为常量,都返回const_iterator。

任何可能改变容器对象容量的操作,都会使该对象的迭代器失效。

常见的坑就是循环时erase元素,导致可能误操作已失效的迭代器。

迭代器运算

迭代器实际上是分类的,string和vector的迭代器是随机访问迭代器,它提供了额外的一些操作:

- -
iter + n 迭代器加上一个整数值仍得一个迭代器,迭代器指示的新位置与原来相比向前移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一位置
iter - n 迭代器减去一个整数值仍得一个迭代器,迭代器指示的新位置与原来相比向后移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一位置
iter1 += n 迭代器加法的复合赋值语句,将iter1加n的结果赋给iter1
iter1 -= n 迭代器减法的复合赋值语句,将iter1减n的结果赋给iter1
iter1 - iter2 两个迭代器相减的结果是它们之间的距离,也就是说,将运算符右侧的迭代器向前移动差值个元素后将得到左侧的迭代器。参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一位置
>, >=, <, <= 迭代器的关系运算符,如果某迭代器指向的容器位置在两一个迭代器所指位置之前,则说前者小于后者。参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一位置

difference_type类型用来表示两个迭代器间的距离,这是一种带符号整数类型。

掌握iterator,要读STL源码。

数组

C已经有了,C++继承过来,没什么变化,但由于C++有引用语法,所以派生了一些复杂的写法。

###定义和初始化内置数组

定义数组的时候必须指定数组的类型,不允许用auto关键字由初始值列表推断类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//定义
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就正确

//初始化
const unsigned sz = 3;
int ial[sz] = {0, 1, 2}; //含有3个元素的数组,分别为0,1,2
int a2[] = {0, 1, 2}; //3个元素的数组,显示初始化定义可以省略sz
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}; //错误

字符数组比较特别,从C开始就对其语法进行了简化,不过这也导致了很多新手字符常量和字符数组傻傻分不清楚。

1
2
3
4
char a1[] = {'C','+','+'};	// 列表初始化,没有空字符
char a2[] = {'C','+','+','\0'}; // 列表初始化,含有显式的空字符
char a3[] = "C++"; // 自动添加表示字符串结束的空字符
const char a4[6] = "Daniel"; // 错误:空间不足

数组不能直接拷贝和赋值,嗯,毕竟沿用C。

1
2
3
int a[] = {0, 1, 2};	
int a2 = a; // 错误:不能用一个数组初始化另一个数组
a2 = a; // 错误:不能把一个数组赋值给另一个数组

关于这一点,新人可能对string之类的复合类型可以拷贝初始化以及赋值复制的欣慰有所困惑,实际上那都是由其class背后的显式或隐式的拷贝构造器和赋值操作符完成的。而数组是个很特别的东西,游离于三界之外,有自己继承自C的一套语法,不能直接套基础类型和复合类型的使用范式。

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

数组的迭代器就是原生的指针,数组和指针就是容器和迭代器的原始模型。

C++11在头文件iterator中定义了两个名为beginend的函数,功能与容器中的两个同名成员函数类似,参数是一个数组。

1
2
3
int ia[] = {0,1,2,3,4,5,6,7,8,9};	// ia是个包含了10个int的数组
int *beg = begin(ia); // 指向ia的第一个元素
int *last = end(ia); // 指向ia的最后一个元素

两个指针相减的结果类型是ptrdiff_t,这是一种定义在头文件cstddef中的带符号类型。

###C风格字符串

C风格字符串和string的混用也是经常性的,一般来说哪种便于处理,就用哪种形式处理。

string具有通过C风格字符串作为参数的构造器,所以string s("Hello World");这种写法是可以的,但是反过来,char *str = s;就不行了,因为基础类型背后可没有所谓的拷贝构造来支持。

对应string s = "Hello World";这种写法,实际上是拷贝初始化,而拷贝初始化会先调用参数为char*的构造器来生成临时的string对象,然后临时对象再传给s的拷贝构造器。

那么对于string到C风格字符串的转换,是由string的成员函数c_str()来完成的:const char *str = s.c_str();

从C++的设计者角度来说,是想让玩家尽量不使用C的数组啊、C风格字符串啊、指针啊等东西,但是实际上这么多年过去了,从标准库的设计到C++的整个生态环境都不尽人意,有些东西还是避不开,样样具备的实验室需要你对每一种语法糖的底层机理都了如指掌才能在使用时左右逢源,这也是C++为什么难用的原因。

##多维数组

除了一维数组以外,还可以有多维数组,和C一样,不过是数组嵌套数组罢了。

1
2
3
4
5
6
7
8
9
10
11
12
int ia[3][4] = 
{ // 三个元素,每个元素都是一个尺寸为4的数组
{0, 1, 2, 3}, // 行索引0的初始化
{4, 5, 6, 7}, // 行索引1的初始化
{8, 9, 10, 11} // 行索引2的初始化
};
// 不使用可选的嵌套花括号的初始化方式,与上面是等价的
int ib[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
// 显示初始化每个数组元素的第一个元素
int ic[3][4] = {{ 0 }, { 4 }, { 8 }};
// 显示初始化第一个数组元素,其他数组元素会被默认初始化
int id[3][4] = {0, 3, 6, 9};

多维数组的寻址,不过是在上一个维度基础上追加乘法偏移而已。

使用范围for语句处理多维数组时,为了避免数组被自动转换成指针,语句中的外层循环控制变量必须声明成引用类型。

1
2
3
for(const auto &row : ia)
for(auto col : row)
cout << col << endl;

也算是历史遗留问题了,因为数组这种设计在C++里总显得格格不入。

如果row不是引用类型,编译器初始化row时会自动将数组形式的元素转换成指向该数组内首元素的指针,这样得到的row就是int*类型,而之后的内层循环则试图在一个int*内遍历,程序将无法通过编译。

声明指向数组类型的指针时,必须带有圆括号(因为优先级的问题)。

1
2
int *ip[4];     // ip是包含4个int指针类型的数组
int (*ip)[4]; // ip是指向四个int元素的数组的指针

使用auto和decltype能省略复杂的指针定义。

1
2
3
4
5
6
7
8
9
// 输出ia中每个元素的值,每个内层数组各占一行
// p指向含有4个int的数组
for (auto p = ia; p != ia + 3; ++p)
{
// q指向4个int数组的首元素,也就是说,q指向一个int
for (auto q = *p; q != *p + 4; ++q)
cout << *q << ' ';
cout << endl;
}

选用begin和end替换:

1
2
3
4
5
6
7
8
// p指向ia的第一个数组
for(auto p = begin(ia);p!=end(ia);++p){
// q指向内层数组的首元素
for(auto q = begin(*p);q!=end(*p);++q){
cout << *q << ' '; // 输出q所指的int值
}
cout << endl;
}

再通过类型别名简化:

1
2
3
4
5
6
7
8
9
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. string, vector, array
    1. 1.1. 命名空间的using声明
    2. 1.2. string
      1. 1.2.1. 处理string对象中的字符
    3. 1.3. vector
      1. 1.3.1. 定义和初始化vector对象
      2. 1.3.2. vector相关操作
    4. 1.4. 迭代器
      1. 1.4.1. 迭代器运算
    5. 1.5. 数组
,