前言
上一章,复习了基本数据类型,趁热打铁,继续把字符串、向量(vector)和数组搞定。
字符串是由字符组成,也是序列结构,因此,本章其实设计到的数据结构是序列。那么就需要考虑是否能够随机访问,如何遍历,如何进行一些基本操作,以及这些基本操作的效率问题。
整个系列的复习会将所有涉及到的stl进行分析,工作量可能会大,但是复习嘛,总是一步一个脚印。废话不多说,开始干货。
3.1 using声明命名空间
命名空间这个东西使得大的项目的名称管理的效率得到了提升。如果需要使用库函数,那么需要加上命名空间才能使用相应的名称。而using就使得不用每个名称前都需要加命名空间。
#include <iostream>
using std::cin; // 一般来说,大家都喜欢之间using namespace std,这样就不用对每个对象都使用using了
int main() {}
既然提到了#include,我觉得有必要复习一下预处理器命令,以及宏,这个还是非常重要,我在复习设计模式的,涉及到的一个问题,c++如何实现反射?,详细请见博客另一篇文章《C++反射的实现》
注意:
头文件里不应该包含using声明,因为,头文件的内容会拷贝到所有引用它的文件中,如果头文件里有某个声明,那么每个使用了这个头文件的文件都会有这个声明,名称污染。。。不知哪听来的
3.2 string类型
此处的string类型是标准库中的string,定义在std命名空间里。导入的头文件是string,而不是string.h,后者是c标准下的字符串数组的库函数集合。
#include <string>
// 注意,不是string.h,如果想要使用c下的字符串处理函数,使用后者
using namespace std;
string类型是可变长的字符序列。
- 初始化
string s1; // 默认初始化,空串
string s2 = s1; // s2 是 s1 的副本
string s2(s1); // 直接初始化
string s3("hiya");
string s3 = "hiya"; // s3 是 字面值的副本
string s4(10, 'c'); // s4 "cccccccccc"
- 操作
os << s // 写入输出流
is >> s // 从流读出字符串,以空白分隔
getline(is, s) // 从流中读出一行
s.empty()
s.size()
s[n]
s1 + s2
s1 = s2
s1 == s2
s1 != s2
<, <=, >, >=
s.push_back(char s)
s.pop_back()
s.begin()
s.end()
s.rbegin()
s.rend()
s.reverse()
s.clear()
s.back()
s.front()
s += "hello"
s.find(str, pos) // 没找到返回-1, 返回string::size_type类型,保证能够在当前机器上存储string对象大小
注意:
string.size() 返回的是size_type类型,这是一个无符号类型,因此,在比较时要特别注意隐式转换(有符号类型会隐式转换成无符号类型),我们知道找不到返回的是size_t 类型的 -1,这个数其实是可能的最大数。
另外string类型的加法,必须要有一个运算对象是string,(字符串字面值不是字符串string类型)
注意:
s = "hello" + "," + s; // 错误,因为"hello"是一个字符串字面值,不是字符串
- 处理string对象中的字符
#include <cctype>
string s = "hello world";
for (auto &c : s) { // 要修改字符,必须要使用引用
cout << isalnum(c) << endl;
tolower(c);
}
char a = 'a';
isalnum(a);
isalpha(a);
isdigit(a);
isspace(a);
tolower(a);
toupper(a);
3.3 标准库类型vector
vector是标准库里的一类容器,表示对象的有序集合(不包含引用)。
- 定义和初始化vector对象
vector其内存占用空间是只增不减,erase和clear都不能减少vector占用的内存。所有内存空间在vector析构的时候回收。
#include <vector>
using namespace std;
vector<T> v1;
vector<T> v2(v1);
vector<T> v2 = v1;
vector<T> v3(n, val);
vector<T> v4(n);
vector<T> v5{a, b, c};
vector<T> v5 = {a, b, c};
vector<string> v6 = {10, "1"}; // 十个string对象
vector<int> v7 = {10, 1}; // 两个对象
- 操作
vector<int > arr;
arr.push_back(10);
arr.pop_back();
arr.empty();
arr.size();
arr.front();
arr.back();
vector<int> foo(3, 100);
foo.swap(bar); // swap 是常数时间复杂度(仅仅交换了指针),因此常常用来清空vector,进行内存释放
for (auto &e : arr) {
e = 11; // 可以进行修改,但不要对arr进行添加或者删除
}
前面提到了vector怎样清除内存占用的问题。这里就要用到临时对象以及析构的概念了。
vector<int> arr(100000, 0);
...
//建立临时对象,然后调用swap函数,临时对象会立即调用析构函数,进行内存释放
vector<int>().swap(arr);
a.pop_back();
vector<int>(arr).swap(arr); // 释放一个
注意:
vector 使用下标访问元素,必须是已经存在的下标,不要放低级错误
3.4 迭代器介绍
迭代器在标准库中是一种访问容器对象更加通用的方式,只有少数几个容器是支持下标访问的,目前已知的有vector、string、deque、map、unordered_map。
- 使用迭代器
auto b = v.begin();
auto e = v.end(); // 记住auto的方法,非常简便,不用写过多的类型
*iter // 返回对象引用
iter -> mem // 取名为mem的成员
++ iter
-- iter
iter1 == iter2
iter1 != iter2
注意:
再次提醒,任何改变容器对象数量,都会使得迭代器失效
- 迭代器运算
iter + n
iter - n
iter += n
iter -= n
iter1 - iter2
>, >=, <, <=
auto mid = vi.begin() + v.size() / 2; // 取中点
3.5 数组
数组类似vector,但是数组的大小在定义时就已经确定了。
注意:
数组不允许使用 auto 关键字由初始化列表来推断类型
- 定义和初始化
int a[10]; // 默认初始化
// 显示初始化
int arr[5] = {1, 1}; // 1 1 0 0 0
int arr[] = {1, 1} // 1 1
int arr[2] = {1, 2, 3} // 错误,初始值过多
int arr[100] = {0} // 0 ...
// 字符数组,特殊在最后的空字符
char a1[] = {'a', 'b', 'c'} // 最后没有空字符
char a2[] = {'a', '\0'} // 最后有显示的空字符
char a3[] = "hello"; // 自动添加末尾空字符
char a4[5] = "hello word" // 错误,没有空间存放空字符
- 理解复杂数组
int &ref[10] = ptr; // 错误,没有引用数组
int *ptr[10]; // ptr是一个数组,包含10个int *类型指针的数组
int (*ptr)[10]; // ptr是一个指针,指向含有10个int类型整数的数组
int (&ptr_ref)[10]; // ptr_ref是一个引用,绑定的是含有10个int类型整数的数组
- C 风格字符串(安全风险,需要自己管理大小)
C 风格的字符串要求字符串数组要以空字符’\0’结尾。
#include <string.h>
strlen(p);
strcmp(p1, p2); // 返回比较值,+、-、0
strcat(p1, p2); // p2 附加到p1 ,返回p1
strcpy(p1, p2); // p2 拷贝给p1,返回p1
3.6 多维数组
严格来说,c++没有多维数组,通常说的多维数组都是数组的数组。因此,初始化、访问时都可以仔细理解一下。
int ia[3][4]; // 默认初始化,大小为3的数组,每个元素是4个整数的数组
int arr[100][200] = {0} // 初始化为0
int ia[3][4] = {
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10 , 11}
};
// 等价于
int ia[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
// 显示初始化每行首元素
int ia[3][4] = {{0}, {3}, {4}};
// 显示初始化第一行,其他的值初始化
int ia[3][4] = {0, 3, 6, 9};
- 指针和多维数组
时刻记住,多维数组实际上是数组的数组。
int ia[3][4];
int (*p)[4] = ia; // p 指向第一行的数组
p = &ia[2]; // p 指向ia的末尾行
// 值得注意的是,p是一个指向有四个整数的数组的指针,而数组ia就是一个指针,因此p是一个指针的指针
// 比如要找ia[1][1]
*(*(ia + 1) + 1);
*(ia[1] + 1);
*(&ia[0][0] + 4 * 1 + 1);
a // 等价于a[0],保存的是指向第一行的数组的指针, 也就是a[0][0]
a + 1 // 等价于a[1], 保存的是指向第二行的数组的指针, 也就是a[1][0]
- 可以使用typedef和using简化多维数组的指针
using int_array = int[4];
typedef int int_array[4];