
大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~
vector这个单词还有向量的意思,但其底层就是顺序表

先看图中vector的模板声明: class T:这是元素类型的参数。我们可以传入任意类型(如int、double、自定义类Person等),vector会据此实例化出 “存储该类型元素的动态数组” class Alloc = allocator< T >:这是内存分配器的参数(默认使用 STL 的allocator< T >)。分配器负责vector的内存分配与释放,这个参数让vector的内存管理更灵活(一般场景用默认分配器即可),若觉得库中的内存池在特殊情况下不符合自己的要求,也可以自己写一个
注意:这里的vector与string有一些不同 std::vector本身就是类模板,使用时必须显式指定类型参数(如vector< int >、vector< double >),否则无法直接使用; std::string不是模板,它是basic_string模板针对char类型的具体实例(通过typedef定义的别名),使用时无需指定类型参数(直接写string即可)。
vector的接口设计和string非常相似,但是也有自身的一些特点

看一下STL中的vector提供的4个构造版本

第二个版本

第三个版本

这里迭代器区间构造给的参数是一个模板,也就是说可以传自己的迭代器,也可以传其他的迭代器。
第四个版本



再看一下C++11提供的一个额外接口,该接口就支持用一个花括号列表(内部无论几个值都可以)去初始化vector的对象了


{1,2,3,4,5}初始化v6、v7时,是调用vector的initializer_list构造函数:编译器先把{1,2,3,4,5}封装成initializer_list< int >类型的临时对象 il,然后将这个对象作为参数传递给vector的 initializer_list 构造函数,最终完成v6和v7的元素初始化
initializer_list 底层的内部就是两个指针,可以说是在栈上开了一个临时数组把花括号内的值存下来,两个指针分别是开始的指针和指向最后一个数据的下一个位置的指针,成员函数size就是两个指针相减


其begin是个迭代器(本质上迭代器的行为是模拟指向数组指针的行为),也意味着 initializer_list 可以用范围for,范围for本质上就是转换为用迭代器(也有begin相关类似指针的东西)


内部的底层就是相当于写一个范围for遍历对象 il,然后将结果push_back到vector中
其他容器也可以这样初始化,其中string用起来比较鸡肋,所以前面的文章没写
这里详细补充一下explicit关键字的作用 如果一个类的构造函数是单参数(或除了第一个参数外其余参数都有默认值)的,C++ 允许“隐式类型转换”—— 即可以用该参数的类型 “直接赋值” 给类对象,编译器会自动调用构造函数完成转换。
没有explicit的情况:
class MyString {
public:
// 单参数构造函数:用一个int创建指定长度的字符串
MyString(int length) {
// 假设这里是创建length个字符的逻辑
}
};
void printString(MyString s) { /* 打印字符串 */ }
int main() {
// 隐式转换:编译器会自动调用MyString(5),把int 5转换成MyString对象
printString(5);
// 等价于 printString(MyString(5));
return 0;
}这种隐式转换可能违背开发者的意图—— 比如printString期望的是MyString类型,但传入int也能运行,容易隐藏逻辑错误(比如开发者可能误把 “长度” 传成了其他含义的int)。
当构造函数被explicit修饰后,编译器会禁止这种 “隐式类型转换”,必须显式调用构造函数才能创建对象。 上面的例子(添加explicit):
class MyString {
public:
// explicit修饰后,禁止隐式转换
explicit MyString(int length) {
// 构造逻辑
}
};
void printString(MyString s) { /* 打印字符串 */ }
int main() {
// 错误!编译器不允许隐式转换,会提示类型不匹配
// printString(5);
// 必须显式调用构造函数,意图更明确
printString(MyString(5));
return 0;
}说粗糙一些就是打印,库中的vector不支持流插入流提取(库中没有重载对应的函数),编译器不知道怎么输出vector中的数据。库中的string是支持的,string就是把串中的字符依次输出即可
//不支持,库中没有重载对应的运算符
//cout << v2 << endl;想打印要自己手搓

如图x的ASCII码值未120

可以看到vector和string也一样保持稳定的1.5倍扩容,string的扩容第一次是2倍(小于15的时候是存在栈上的_buff数组中,_buff满了之后直接在堆上开了2倍的空间,后面保持1.5倍) 也就是数组结构的增长扩容,VS一贯的方式还是1.5倍扩容
若提前知道插入100个数据就可以使用reserve减少扩容提高效率

vector和string的reserve还有所不同,string的reserve还会对齐,需要100的时候实际上还可能多开一点,vector容器中要100开100
下面在Release模式下测试一下效率的提升


且vector的reserve是不会缩容的

向缩容调用下面的接口

可以缩容到size大小
resize可以插入和删除数据

reserve开空间是不插入数据的,resize就是开空间+初始化的效果,已经有值的话已经有的数据不会动,而是会在后面插入数据


std::vector::at:C++ 标准规定其总是会做边界检查,若索引越界会抛出out_of_range异常。 std::vector::operator[]:C++ 标准未要求其做边界检查,但在 VS 系列编译器的debug 模式下,为了便于调试,该运算符也被实现了边界检查。
string提供的头插头删insert和erase主要是通过下标和长度来控制



vector主要通过迭代器来控制

下面展示一下隐式类型转换的魅力

结构体 AA 的构造函数接受两个 int 类型参数(多参数的构造函数支持隐式类型转换),且没有被 explicit 修饰。这是允许 “隐式类型转换” 的关键(explicit 会禁止构造函数的隐式调用)
代码中 vector< AA > v = { {1,1}, {2,2}, {3,3} }; 的初始化逻辑:对于每个内部列表 {1,1}、{2,2}、{3,3},编译器会隐式调用 AA 的构造函数,将两个 int 值转换为 AA 类型的对象。例如,{1,1} 会被隐式转换为 AA(1, 1),这属于 “用户定义的隐式类型转换”(通过类的构造函数实现从 (int, int) 到 AA 的类型转换)。这些转换后的 AA 对象,再通过 vector 的 initializer_list 构造函数,最终初始化 vector< AA > 的元素。
尤其注意这里的两套花括号:外围的花括号是 initializer_list,内层的花括号是多参数构造函数的隐式类型转换

这里迭代器访问编译不通过的原因: 代码中vector< AA > v的迭代器it解引用后是AA类型的对象(*it是AA类型) std::cout默认只支持内置类型(如int、double等)和标准库类型的输出,对于自定义结构体AA,必须显式重载operator<<,才能让cout知道如何打印它
这里有两种解决方案:第一种就是重载一个流插入运算符
struct AA
{
int _a1 = 1;
int _a2 = 1;
AA(int a1 = 1, int a2 = 1) : _a1(a1), _a2(a2) {}
};
// 重载输出运算符,让cout可以打印AA对象
std::ostream& operator<<(std::ostream& os, const AA& aa) {
os << "(" << aa._a1 << ", " << aa._a2 << ")";
return os;
}第二种方案:既然不能直接输出自定义结构体AA的对象,那就通过迭代器访问结构体的成员变量_a1和_a2(这两个成员是int类型,cout可以直接输出int类型),从而绕开了 “需要重载operator<<来支持自定义类型输出” 的要求。这种方式的核心是直接操作结构体的内置类型成员

这里->能直接访问结构体的内置类型成员的原因如下: vector< AA > 的迭代器(如代码中的 it)被设计为 **“行为类似指针” 的对象 **,标准库为迭代器重载了 -> 运算符。 当执行 it ->_a1 时,迭代器的 -> 运算符会返回一个指向其内部管理的 AA 对象的指针(相当于 &( *it)),然后通过这个指针访问 AA 的成员 _a1。 本质上等价于:( *it)._a1(先解引用迭代器得到 AA 对象,再通过 . 访问成员),而 -> 是这种操作的简化语法。
C++ 中,结构体(struct)的成员默认是公有(public)的(类 class 默认私有)
二者的作用都是尾插

这里略微解释一下二者的区别:
简单来说,emplace_back 最大的优势是支持 “原地构造”—— 当传入构造参数(如 1,1)时,能直接在 vector 内部创建对象,省去了 “临时对象构造 + 拷贝” 的步骤,性能更优;而 push_back 更依赖 “先有对象,再拷贝入容器” 的逻辑

//且不支持交叉使用
//emplace_back的函数参数是个模板,推不出来
v.emplace_back({ 1,1 });
v.push_back(2,2);#define _CRT_SECURE_NO_WARNINGS 666
#include<iostream>
#include<vector>
using namespace std;
void Print(const vector<int>& v)
{
for (size_t i = 0; i < v.size(); i++)
{
cout << v[i] << " ";
}
cout << endl;
//for (auto e : v)
//{
// cout << e << " ";
//}
//cout << endl;
////形参的vector带const修饰,要用const迭代器
//vector<int>::const_iterator it = v.begin();
//while (it != v.end())
//{
// cout << *it << " ";
// ++it;
//}
//cout << endl;
}
void test_vector1()
{
vector<int> v1;//构造空的vector
vector<int> v2(10, 1);//用n个value构造
//传自己的迭代器区间构造
vector<int> v3(v2.begin(), v2.end());
string s1("xxxxxxxxxxxx");
//传其他容器的迭代器区间构造,迭代器构造没有expicit
//类型可以转换,且char可以隐式转换为int
//大类型转小类型可能溢出(改变值),小类型转大类型没有这种情况
vector<int> v4(s1.begin(), s1.end());
vector<int> v5(v3);
//vector<int> v6({ 1,2,3,4,5 });
vector<int> v6 = { 1,2,3,4,5 };
vector<int> v7 = { 1,2,3,4,5,1,1,1,1,1,1 };
//不支持,库中没有重载对应的运算符
//cout << v2 << endl;
Print(v2);
Print(v4);
Print(v6);
Print(v7);
auto il = { 10,20,30,1,2,3 };
for (auto e : il)
{
cout << e << " ";
}
cout << endl;
}
void test_vector2()
{
vector<int> v1;
//const int n = 100000000;
const int n = 100;
//v1.reserve(n);
size_t old = v1.capacity();
//cout << v1.capacity() << endl;
size_t begin = clock();
for (size_t i = 0; i < n; i++)
{
v1.push_back(i);
//if (old != v1.capacity())
//{
// cout << v1.capacity() << endl;
// old = v1.capacity();
//}
}
size_t end = clock();
cout << end - begin << endl;
vector<int> v2;
v2.resize(100, 1);
Print(v2);
}
void test_vector3()
{
vector<int> v1 = { 1,2,3,4,5 };
v1.push_back(6);
Print(v1);
//头插
v1.insert(v1.begin(), 0);
Print(v1);
//vector的迭代器是支持+的,在下标为3的位置插一个0
v1.insert(v1.begin() + 3, 0);
Print(v1);
//头插和中间插入因为要挪动数据都是有些代价的
//头删
v1.erase(v1.begin());
Print(v1);
v1.erase(v1.begin() + 3);
Print(v1);
}
struct AA
{
int _a1 = 1;
int _a2 = 1;
AA(int a1 = 1, int a2 = 1)
:_a1(a1)
, _a2(a2)
{
}
};
//template<class T>
//class vector
//{
//private:
// T* _a;
// size_t _size;
// size_t _capacity;
//};
void test_vector4()
{
AA aa1 = { 0,0 };
vector<AA> v = { aa1,{1,1},{2,2},{3,3} };
auto it = v.begin();
while (it != v.end())
{
cout << it->_a1 << ":" << it->_a2 << endl;
++it;
}
cout << endl;
v.push_back(aa1);
v.emplace_back(aa1);
//差异,推荐
v.emplace_back(1, 1);
v.push_back({ 2,2 });
it = v.begin();
while (it != v.end())
{
cout << it->_a1 << ":" << it->_a2 << endl;
++it;
}
cout << endl;
}
int main()
{
//test_vector1();
//test_vector2();
//test_vector3();
test_vector4();
return 0;
}