前言
本章内容相比第二章要简单不少,里面比较重要的内容主要是vector和迭代器,这里只是很简单的介绍了一下,在后续的章节会有更详细、复杂的说明。以下记录的都是比较重要或者易混淆的知识点,对于像string、vector只列举了部分方法的例子。
文章目录
- 一、string
- 1.1string初始化和常见操作
- 1.2string对象字符处理API
- 二、标准库类型vector
- 2.1vector常见操作
- 三、初识迭代器
- 3.1使用迭代器
- 3.2迭代器运算
- 四、数组
- 4.1初始化
- 4.2访问数组元素
- 4.3指针和数组
一、string
string
是C++的一个标准库类型,表示可变长的字符序列。string 定义在命名空间std中
。
1.1string初始化和常见操作
初始化
下表是string
的初始化方式。值得注意的是当我们使用=
号实际上执行的是拷贝初始化,反之就是直接初始化。
string对象操作
下表是几种string的操作方式。
对于读写string对象,在使用cin
读取一个对象时会自动忽略开头的空白(空格、换行符、制表符等),直到遇到下一个空白为止。如下,当我们输入一段字符串" hhhh yyyy "
,程序自动忽略开头的几个空格并在遇到下一个空格后停止读入,所以最后的结果为hhhh
。在遇到字符串类型的算法题时,使用cin可以减轻处理字符串的工作。
string s1;
cin >> s1; // 输入" hhhh yyyy ";
cout << s1; // 输出 "hhhh"
如果我们想要保留输入的一整段字符串,可以使用getline
来读取一整行数据。如下,getline
需要两个参数,一个是输入流,另一个是string
对象,在读取的过程中,会把整段数据包括换行符都读在缓冲区中,但是存储的时候会舍弃掉换行符,所以最后的结果并没有包含换行符,这也是为什么在输出时要加上endl
,endl可以结束当前行并刷新显示缓冲区。
getline(cin,s1); //输入" hhhh yyyy ";
cout << s1 << endl; // 输出" hhhh yyyy ";
size()
会返回当前字符串的一个大小,大部分和我一样的初学者都会想当然的以为返回的类型是一个int
,但其实类型是string::size_type
。size_type
是一个无符号类型的值,能够存放任何string
对象的大小。【代码风格:在循环访问一个字符串对象时,标识符的类型尽量定义成size_type
,这样该标识不可能为负数从而减少越界等错误】。在上一章中学过auto和decltype
,我们在接收一个字符串对象的长度时可以用它们来定义变量。
auto len = s1.size(); // len 的类型是size_type
两个字符串比较首先看长度是否相等,如果长度不一样,不一定就是较长的字符串大,因为string
对象比较遵循第一相异字符比较结果,也就是说如果第一个不同字符,较长字符串改字符的字典序小于较短字符串,那么最后的结果就是较长字符串小于较短字符串。如下。
string str = "Hello";
string str2 = "Hiya"; // str2 > str
标准库允许把字面值或字符字面值转换为string
对象,前提是‘+’两边至少包含一个string对象。
string str = "123";
string str2 = str + "," + "456"; // 123456
string str3 = "123" + "456"; // 错误,+两边不存在string对象
1.2string对象字符处理API
要处理一个字符那么首先要判断它的类型,在标准款中提供了一个cctype
函数,这是C++
兼容C语言
的一个头文件,如下有一些判断字符类型的函数。
一般来说,我们都是从一个字符串中循环提取一个字符进行处理,C++
提供了一个很方便的循环语句,范围for语句
,包括在后面迭代器等章节都会经常使用,定义如下。
for(auto x : str); // 循环取出原串的一个字符,不改变原串
for(auto &x : str); // 通过引用的方式可以改变原串
如果我们只需要处理一个字符串中的某个字符,可以使用下标运算符[index]
,里面接收的参数类型是size_type
,保证大于等于0。我们也可以直接通过[index]
改变相应位置上的值。
混用C风格字符串
C语言定义字符串是通过char *
的方式,但是我们不能把一个string
对象拿来初始化C语言
风格的字符串。需要使用c_str
转换,它返回的是一个指针,指向一个以空字符串结束的字符数组,这个数组存储的内容就是我们定义的string
对象,返回的类型为const char *
来保证不会改变字符数组的内容。
string str("asdasd");
char *str2 = str; // 错误
const char*str2 = str.c_str()
二、标准库类型vector
vector
表示对象的集合,里面的对象类型都是一致,每个对象对应一个索引,用于访问该对象。vector
是一个类模板,在后续的章节有专门一章来讲解模板,感觉比较难懂。除了类模板,还有一个就是函数模板,模板本身不是类或函数,编译器根据模板生成类型和函数的过程称为实例化,
2.1vector常见操作
初始化
定义和初始化的方法不同的API都差异不大。注意几点,当使用拷贝初始化=
时,只能提供一个初始值;如果这个初始值是类内初始值(一个class
定义的值),只能使用拷贝初始化或使用花括号的形式初始化;使用列表初始化是用{}
。
在定义一个vector
时我们可能会给出一个大小n
,此时程序会进行值初始化,如果 当前vector
的类型是int
,那就会初始化n
个0。
总的来说,()
可以说是根据提供的值来构造对象,{}
是根据提供的值来初始化对象,只有在无法初始化时才考虑其它方式。注意下面的表达式vector v4{10}。
vector<int> v1(10); // 构造10个大小的整形容器
vector<int> v2{10}; // 容器含有一个元素10
vector<string> v3("hi"); //错误,不能使用字面值构建vector对象
vector<string> v4{10}; // 字面值10并不是string对象,所以不能拿来初始化,而是直接构造含有10个空串的容器
添加元素
push_back
可以往一个容器的尾部添加一个元素。vector
和C语言
中最大的区别就是定义的时候可以不指定容量,vector
可以根据当前存储的元素动态的改变容器的大小,是否要在定义时指定容量大小会在后续的章节中谈到,vector
提供了一些方法允许我们提升动态添加元素的性能。
vector<int> vec;
vec.push_back(1);
cout << vec[0]; // 1 可以通过索引访问值
vec[1] = 3; // 错误,不能通过索引的添加值
其它的操作如下表所示,都是比较简单的函数,这里就不一一举例了。记住v.size()返回的类型还是size_type。【代码风格:如果总是忘记定义size_type去接收容器的大小,可以使用上一章学的auto/decltype自动获取】。
三、初识迭代器
前面我们知道可以通过下标运算符来访问string
对象或者vector
对象的元素,使用迭代器也能达到此目的。除了vector
外,其它的一些容器也提供了迭代器,但是部分缺不支持下标运算符,string
对象虽然不是容器,但是也能使用迭代器。
3.1使用迭代器
begin和end
是常用的两个迭代器成员,begin
指向容器的第一个元素,end
指向容器尾部后一位,这个位置不存在元素,有点类似链表最后的null
,仅仅作为一个标志。对于一个空的容器,它的begin和end都返回同一个迭代器。
下面是常用的迭代器的运算符。*iter
返回的一个元素的引用,如果要访问该元素下对应的成员还得使用iter->mem(或者(*iter).mem,()不能少)
进行解引用。移动当前迭代器使用++或--
。
迭代器类型
迭代器也有不同的类型,定义一个迭代器vector<int>::iterator it
,通过it
可以去读写vector<int>
的元素;常量迭代器vector<int>::const_iterator it2
则只能进行读操作。如何确定迭代器的类型依据对象的类型,如果对象是一个常量,那么只能使用常量迭代器,反之都可以。同理,我们在上面使用beging和end
返回的迭代器类型也是根据对象的类型。如果对于一个非常量容器,我们想得到一个常量迭代器,可以使用C++11
引入的新特性cbegin和cend
,直接返回一个常量迭代器。
解引用迭代器可以获得迭代器所指的对象,如果对象的类型是一个类,我们可以通过iter->mem或者(*iter).mem
访问类的成员函数。上面说过使用(*iter).mem
方式时()不能少,如下说明。
vector<string> vec{"hhhhh"};
vector<string>::iterator it = vec.begin();
(*it).empty(); // false
*it.empty(); // it是一个迭代器,没有empty成员,所以错误
使用->
相当于把解引用和访问成员融合在一起,更加的方便。
前面提到,vector
会动态的增长容器的大小,所以我们在使用迭代器遍历容器元素时不能向容器中添加元素,否则当前的迭代器会失效。总之当你定义了一个迭代器,任何将会改变原容器的操作都会使当前的迭代器失效。
3.2迭代器运算
除了++和--
让迭代器移动一步,我们还可以使用下表的方法进行多步移动。注意一下当两个迭代器相减时,返回的是这两个迭代器的距离,返回的类型是difference_type
(带符号整形数)。很明显这个距离可正可负。
四、数组
数组也是一个能存放不同对象的容器,与vector
不同,数组的大小一旦确定就不能改变,不能随意向数组中增加元素,在某些情况使用数组可以增加程序的运行性能,但是灵活性也大大的降低。
4.1初始化
数组是一种符合类型,声明一个数组如a[d]
,其中a
是数组的名字,d
是数组的维度,维度是数组元素的个数,它必须要是一个常量表达式。
unsigned cnt = 12; //非常量表达式
int arr[cnt]; // 错误
constexpr unsigned cnt1 = 12;
int arr[cnt2]; // 定义一个含有12个元素的整形数组
和内置类型的变量一样,在函数外部定义了一个数组将会根据类型进行默认初始化,在内部则会是未定义。在定义数组的时候必须要指定数组的类型,不能使用auto去根据初始值列表推断类型。
不允许拷贝和赋值
我们不能用一个数组去初始化另外一个数组,虽然一些编译器可能支持这个操作,但是为了程序的兼容尽量根据标准来书写代码。但是我们却可以使用数组来初始化一个vector
。(begin(arr), end(arr)在4.3中有讲解)
int arr[] = {1,2,3,4,5,6,67};
vector<int> vec(begin(arr), end(arr)); // 含有arr的全部元素
vector<int> vec2(arr + 2, arr + 4); // {3,4}
数组本身是一个对象,所以允许定义数组的指针及数组的引用,但是不存在引用的数组。有点绕,直接看书上的例子容易理解一点。
int arr[10];
int *prr[10]; // prr是含有10个指针的数组
int &rrr[10] = ??; // 错误,不存在引用的数组
int (*Prr)[10] = &arr; // Prr指向一个含有10个整数的数组的指针
int (&Rrr)[10] = arr; // Rrr是对数组arr的引用
4.2访问数组元素
与vector、string
相同,数组的元素可以使用范围for
循环或者下标运算符来访问。在使用下标运算符时,返回的类型是size_t
,与size_type
相同也是一个无符号类型。
4.3指针和数组
一般来说,我们对数组名直接取地址编译器会自动的将其替换成数组首元素的地址。所以我们使用auto
自动根据数组名判断时,得到的是一个指针,但是使用decltype
会返回数组的类型,如下。
int arr[10];
int *p = &arr; // 等于 int *p = &arr[0];
auto arr2(arr); // arr2是一个指针
decltype(arr) arr3; // arr3是一个含有10个元素的整形数组
数组虽然没有迭代器,但是数组的指针也可以看成迭代器,迭代器能完成的操作使用指针也能完成。迭代器可以通过begin和end
获得容器的开头和尾后元素,虽然数组不是类类型,没有成员函数,但是我们也可以将数组作为参数传入进去实现同样的效果。
int arr[] = {1,2,3,4,5,56,6};
int *beg = begin(arr); // 指向arr第一个元素的指针
int *last = end(arr); // 指向arr尾后元素的指针
指针的运算与上面迭代器的运算基本一致,两个指针相减返回的类型为ptrdiff_t
。
下标和指针
使用指针的同时也能使用下标访问当前指针操作后指向的元素(当然指向合理的范围)。从p2[-1]
可以看出指针和标准库类型vector
等使用下标运算符的区别,内置类型的下标运算符可以是一个有符号数。
int arr[] = {1,2,3,4,5,56,6};
int i = arr[2]; // 3
int *p = &arr; // p指向第一个元素1
cout << p[2]; // 输出3,相当于p+2
int *p2 = &i;
cout << p2[-1]; // 输出2