🔑🔑博客主页:阿客不是客
🍓🍓系列专栏:从C语言到C++语言的渐深学习
欢迎来到泊舟小课堂
😘博客制作不易欢迎各位👍点赞+⭐收藏+➕关注
一、vector 的了解和介绍
1.1 vector 的介绍
在 C 语言中,数组是一组相同类型元素的有序集合。与字符串类似,它的大小在编译时就已经确定,不可更改。
//大小为5的整型数组
int arr1[5] = { 1,2,3,4,5 };
//大小为5的浮点型数组
double arr2[5] = {0.0};
同样与 string 类似,C++为了更加方便就引入了一个支持可动态大小数组的序列容器 vector。
🔍 vector 文档介绍:vector - C++ Reference
① vector 是表示可变大小数组的序列容器,我们说 vector 像数组,但是又不像数组。
- 说它像数组体现在:vector 就像是数组一样,它也是采用连续存储空间来存储元素的。这也就意味着我们可以用下标访问 vector 的元素,和数组一样的高效。
- 说它不像数组体现在:vector 的大小是可以动态改变的,而且它的大小会被容器自动处理。
② 本质上来说,vector 使用动态分配数组来存储它的元素。
当新元素插入时,为了增加存储空间,这个数组就需要被重新分配大小。具体做法是分配一个新的数组,然后将全部元素转移到这个新的数组。就时间而言,这是一个相对代价较高的任务,因为每当一个新的元素加入后,vector 并不会每次都重新分配大小。
③ vector 分配空间策略:vector 会分配一些额外的空间以适应可能的增长,因此存储空间会比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于末尾插入一个元素时是在常数时间的复杂度完成的。
④ 与其它动态序列容器相比(deques, lists and forward_lists):vector 在访问元素的时候更加高效,在末尾添加和删除元素相对高效。非末尾的删除和插入操作效率低,且统一的迭代器和引用更好。
1.2 vector 的初始化
📜 头文件:<vector>
① 无参构造:一般用的最多的就是无参:
vector<int> v1; // 无参构造,创建一个int类型的,空的vector对象
② 构造并初始化:用 n 个 value 初始化:
vector<int> v2(10, 1); // 10个1
③ 迭代器区间初始化:begin 👉 end:
vector<int> v3(v2.begin(), v2.end());
这种还用什么迭代器,直接用拷贝构造不香吗?
④ 拷贝构造:
vector<int> v3(v2);
但是不乏有这种情况,我不要拷贝对象的头尾数据呢?即,不要它的 begin 和 end,这个时候就得用迭代器了。
vector<int> v3(++v2.begin(), --v2.end());
对于迭代器区间初始化,它这里的 InputInerator 不一定是 vector Inerator。它是一个模板,所以你传的是谁的迭代器,它就可以实例化出谁的迭代器,去遍历 first 和 last 区间,进行初始化。
1.3 string 和 vector 的区别
string 和 vector 有什么区别呢?通过我们之前的学习,string 的底层就是以 char* 来实现的,感觉 vector char 已经很像 string 了。
❓ 我们可以思考一个问题:能不能让 vector char 去替代 string 呢?
答案是否定的。因为 vector char 没有 \0,而 string 结尾是有 \0 的。
vector 是顺序表,存的是任意类型,是针对可动态增长的数组的。而 string 就只是字符串,我随便举两个例子:
- vector 支持正常的增删查改,但是不支持 += (本身也没必要+=),也不支持比较大小。
- vector 也没有 c_str 这些东西,因为 string 作为字符串专用的类,能提供专有的接口(比如 +=,find),所以这就是 string 存在的意义。
二、vector 的基本 使用
2.1 vector 的遍历
2.1.1 下标访问
vector 是连续的空间,又支持 operator[] 和 size() ,所以可以用下标+方括号遍历。
💬 下标 + 方括号:遍历
void test_vector1()
{
vector<int> v1;
vector<int> v2(10, 1);
for (size_t i = 0; i < v2.size(); i++)
{
cout << v2[i] << " ";
}
cout << endl;
}
🚩 运行结果如下:
2.1.2 迭代器
iterator 的使用 | 接口说明 |
begin + end (重点) | 获取第一个数据位置的 iterator/const_iterator, 获取最后一个数据的下一个位置的 iterator/const_iterator |
rbegin + rend | 获取最后一个数据位置的 reverse_iterator, 获取第一个数据前一个位置的 reverse_iterator |
💬 迭代器的读和写:
void test_vector1()
{
vector<int> v1;
vector<int> v2(10, 1);
vector<int>::iterator it = v2.begin();
while (it != v2.end())
{
*it -= 1; // 令每个元素-1
cout << *it << " ";
it++;
}
cout << endl;
}
🚩 运行结果如下:
2.1.3 范围 for
vector 支持迭代器,也就支持范围 for,这个我们在模拟实现 string 的时候已经验证过了。范围 for 的本质就是编译器在编译时自动替换成迭代器,这里也一样。
💬 范围 for 的读和写:
void test_vector1()
{
vector<int> v1;
vector<int> v2(10, 1);
for (auto e : v2)
{
cout << e << " ";
}
cout << endl;
for (auto& e : v2)
{
e += 10;
cout << e << " ";
}
cout << endl;
}
🚩 运行结果如下:
记得范围 for 的写要加引用。
2.2 vector 的空间
容量空间 | 接口说明 |
size | 获取数据个数 |
capacity | 获取容量大小 |
empty | 判断是否为空 |
resize (重点) | 改变 vector 的 size |
reserve (重点) | 改变 vector 放入 capacity |
2.2.1 获取数据个数的 size()
💬 和 string 里的一样,是用来获取数据的个数的。
void test_vector2()
{
vector<int> v(6, 6);
cout << v.size() << endl;
}
🚩 运行结果如下:
2.2.2 改变 vector 容量的 reserve()
💬 reserve:
void test_vector2()
{
vector<int> v(6, 6);
v.reserve(100);
}
reserve 会扩容,但是不会影响数据个数。[capacity] 4 → [capacity]100
2.2.3 改变 vector 大小的 resize()
💬 resize:
void test_vector2()
{
vector<int> v(6, 6);
v.resize(100);
}
string 的 resize 如果不指定 "填充值" ,默认给的是 \0而 vector 的 resize 如果不指定,默认给的是其对应类型的缺省值作为 "填充值",这里是 int 就是 0,如果是指针,对应的缺省值就是空指针。
💬 我们来试着给 resize 提供指定 "填充值":
void test_vector2()
{
vector<int> v(6, 6);
v.resize(100, 6);
}
📌 注意事项:如果开的数据比之前更小,还会删除数据!当然,正如我们 string 章节所说,它的容量并不会因此改变。
2.2.4 vector 空间增长问题
- capacity 的代码在 VS 和 g++下分别运行会发现:VS下 capacity 是按 1.5 倍增长的,而 g++ 下 capacity 是按 2 倍增长的。 这个问题经常会考察,不要固化的认为,顺序表增容都是2倍,具体增长多少是根据具体的需求定义的。VS 是 PJ 版本 STL,g++ 是 SGI 版本 STL。
- reserve 只负责开辟空间,如果确定知道需要用多少空间,reserve 可以缓解 vector 增容的代价缺陷问题。
- resize 在开空间的同时还会进行初始化,影响 size。
💬 测试:
int main()
{
size_t sz;
vector<int> v;
sz = v.capacity();
cout << "making v grow:\n";
cout << "capacity changed: " << sz << endl;
for (int i = 0; i < 100; i++)
{
v.push_back(i);
if (sz != v.capacity())
{
sz = v.capacity();
std::cout << "capacity changed: " << sz << endl;
}
}
return 0;
}
🚩 VS运行结果如下:
🚩 g++ 运行结果如下:
making foo grow :
capacity changed : 0
capacity changed : 1
capacity changed : 2
capacity changed : 4
capacity changed : 8
capacity changed : 16
capacity changed : 32
capacity changed : 64
capacity changed : 128
2.3 vector 的增删查改
vector 增删查改 | 接口说明 |
push_back(重点) | 尾插 |
pop_back (重点) | 尾删 |
find (#include algorithm) | 查找(注意这个是算法模块实现,不是 vector 的成员接口) |
insert | 在 pos 之前插入 val |
erase | 删除 pos 位置的数据 |
swap | 交换两个 vector 的数据空间 |
2.3.1 push_back()尾插
vector 不能用 operator+= ,string 能用 += 主要是 string 不仅可以尾插一个字符还可以追加一个字符串。但是 vector 就只支持一个一个数据的插入和删除,push_back 和 pop_back。
💬 举个例子:
void test_vector1()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
}
2.3.2 assign() 赋值
assign 可以把 vector 的内容覆盖掉。允许给一段区间覆盖,也可以给 个 value 去覆盖。
💬 用 个 value 覆盖:
void test_vector6()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.assign(10, 5); // 原来是1到4,现在改成10个5
}
2.3.3 为什么 vector 类中不提供 find 接口
string、map、set 都有 find() 用,凭什么 vector 和 list 没有?
其实,我们应该考虑的是 —— 为什么 string、map、set 能有 find 操作。而 vector 之所以不提供 find ,是因为如果去查找元素效率就会是 ……
当然,如果我们非要用的话 "algorithm库" 里有通用的 find 操作
#include <algorithm>
该 find 内部是从 begin 到 end 进行一次遍历,其复杂度是 值得一提的是,在C++中,凡是使用迭代器区间,都是左闭右开的 ——
2.3.4 insert() 插入
比如我们刚才用通用 find 找到了 3 的位置,我们想在这个位置前面插入一个数据,就可以使用 insert() 插入。
注意与 string 类不同的是,string 类中 insert 是按照内置的 find 函数返回的下标进行访问的,而vector 没有内置的find 函数,insert 只能使用通用的 find 函数返回的迭代器进行访问,这也导致了经典的迭代器失效的问题,这部分内容我们后续再进行讲解。
void test_vector3()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
vector<int>::iterator ret = find(v.begin(), v.end(), 3);
if (ret != v.end())
{
v.insert(ret, 666);
}
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
🚩 运行结果如下:
2.3.5 erase() 删除
我们我们想删除数据,我们就可以用 erase 去删除。
💬 使用 erase 的时要判断一下有没有找到要删除的目标:
void test_vector3()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
vector<int>::iterator pos = find(v.begin(), v.end(), 3);
if (pos != v.end())
{
v.erase(pos);
}
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
🚩 运行结果如下:
❓ 如果没有判断且数据不存在会怎么样?
void test_vector3()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
vector<int>::iterator pos = find(v.begin(), v.end(), 5);
//if (pos != v.end())
//{
// v.erase(pos);
//}
v.erase(pos);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
如果有了判断,就不会翻车了,如果待删目标不存在,就不会去走 erase() 。 因为 pos 如果找不到就会等于 end() 上的值,我们利用这一点进行 if 判断。
2.3.6 claer() 清空数据
💬 clear:
void test_vector4()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
printf("清空前:");
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
v.clear();
printf("清空后:");
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
🚩 运行结果如下:
三、vector 不支持流插入和流提取
int main()
{
vector<int> v1(10, 0);
//模拟流提取:从键盘提取值
for (size_t i = 0; i < v1.size(); i++)
{
cin >> v1[i];
}
//模拟流插入:往屏幕输出值
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
return 0;
}
四、vector 存储自定义类型
4.1 vector 存储 string 类型
vector 不只是可以存储内置类型,还可以存储自定义类型,比如存储 string 类。
💬这个时候使用范围 for 就需要使用引用来减少拷贝构造了:
int main()
{
//vector存储string
vector<string> v1;
string s("hello world");
v1.push_back(s);
v1.push_back("hello bit"); //隐式类型转换
for (const auto& e : v1) //减少拷贝构造,提高效率
{
cout << e << endl;
}
cout << endl;
return 0;
}
🚩 运行结果如下:
4.2 vector 实现二维数组
vector 既然都能存储 string 类了,那自然也可以存储 vector<int> 类型,这样实现的结果就是二维数组。
4.2.1 传统开辟二维数组
int main()
{
int** p = (int**)malloc(sizeof(int*) * 3);
for (int i = 0; i < 3; i++)
{
p[i] = (int*)malloc(sizeof(int) * 3);
}
for (int i = 0; i < 3; i++)
{
free(p[i]);
p[i] = NULL;
}
free(p);
p = NULL;
return 0;
}
这种方式需要开辟空间,使用有的时候甚至需要二级指针
4.2.2 vector模拟实现二维数组
template<class T>
class vector
{
public:
T& operator[](int i)
{
return _a[i];
}
private:
T* _a;
size_t size;
size_t capacity;
};
int main()
{
//利用vector模拟实现:10 * 5 的二维数组
vector<int> v(5, 1);
vector<vector<int>> vv(10, v);
vv[2][1] = 100;
//vv.operator[](2).operator[](1) = 100;
for (int i = 0; i < 10; i++)
{
for (int j = 0; j < 5; j++)
{
cout << vv[i][j] << " ";
}
cout << endl;
}
return 0;
}