《c++ primer》第三章 字符串、vector、数组
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
前言
本章内容相比第二章要简单不少里面比较重要的内容主要是vector和迭代器这里只是很简单的介绍了一下在后续的章节会有更详细、复杂的说明。以下记录的都是比较重要或者易混淆的知识点对于像string、vector只列举了部分方法的例子。
文章目录
一、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