首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >C++ vector 全面解析:从接口使用到底层机制

C++ vector 全面解析:从接口使用到底层机制

作者头像
云泽808
发布2025-12-30 18:26:18
发布2025-12-30 18:26:18
240
举报

前言

大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~

一、vector的介绍及使用

1.1 vector的介绍

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的使用

vector的接口设计和string非常相似,但是也有自身的一些特点

2.1 构造

在这里插入图片描述
在这里插入图片描述

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

  • 先看第一种vector构造函数中也有内存分配器参数的设计,虽然默认分配器std::allocator< T >能满足绝大多数常规场景,但在一些特殊需求下(如内存池优化、共享内存管理、自定义内存统计等),这时候这个分配器参数的设计就可以供用户自定义分配器来替代默认实现。
在这里插入图片描述
在这里插入图片描述

第二个版本

在这里插入图片描述
在这里插入图片描述

第三个版本

在这里插入图片描述
在这里插入图片描述

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

第四个版本

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

再看一下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用起来比较鸡肋,所以前面的文章没写

2.1.1 explicit关键字

这里详细补充一下explicit关键字的作用 如果一个类的构造函数是单参数(或除了第一个参数外其余参数都有默认值)的,C++ 允许“隐式类型转换”—— 即可以用该参数的类型 “直接赋值” 给类对象,编译器会自动调用构造函数完成转换。

没有explicit的情况:

代码语言:javascript
复制
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):

代码语言:javascript
复制
class MyString {
public:
    // explicit修饰后,禁止隐式转换
    explicit MyString(int length) { 
        // 构造逻辑
    }
};

void printString(MyString s) { /* 打印字符串 */ }

int main() {
    // 错误!编译器不允许隐式转换,会提示类型不匹配
    // printString(5); 

    // 必须显式调用构造函数,意图更明确
    printString(MyString(5)); 
    return 0;
}

2.2 流插入流提取

说粗糙一些就是打印,库中的vector不支持流插入流提取(库中没有重载对应的函数),编译器不知道怎么输出vector中的数据。库中的string是支持的,string就是把串中的字符依次输出即可

代码语言:javascript
复制
//不支持,库中没有重载对应的运算符
//cout << v2 << endl;

想打印要自己手搓

在这里插入图片描述
在这里插入图片描述

如图x的ASCII码值未120

2.3 vector的扩容机制

在这里插入图片描述
在这里插入图片描述

可以看到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大小

2.4 resize

resize可以插入和删除数据

在这里插入图片描述
在这里插入图片描述

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

在这里插入图片描述
在这里插入图片描述

2.5 operator[ ]和at

在这里插入图片描述
在这里插入图片描述

std::vector::at:C++ 标准规定其总是会做边界检查,若索引越界会抛出out_of_range异常。 std::vector::operator[]:C++ 标准未要求其做边界检查,但在 VS 系列编译器的debug 模式下,为了便于调试,该运算符也被实现了边界检查。

2.6 修改相关的接口

string提供的头插头删insert和erase主要是通过下标和长度来控制

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

vector主要通过迭代器来控制

在这里插入图片描述
在这里插入图片描述

三、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知道如何打印它

这里有两种解决方案:第一种就是重载一个流插入运算符

代码语言:javascript
复制
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 默认私有)

3.1 push_back和emplace_back的使用差异

二者的作用都是尾插

在这里插入图片描述
在这里插入图片描述

这里略微解释一下二者的区别:

  • v.push_back(aa1):push_back 接收 AA 类型的对象 aa1,会先拷贝 aa1,再将拷贝后的对象添加到 vector 末尾。
  • v.emplace_back(aa1):传入 AA 对象 aa1 时,emplace_back 会调用 AA 的拷贝构造函数,在 vector 末尾原地构造一个与 aa1 相同的对象(此场景下和 push_back 差异不明显,重点看下面的案例)。
  • v.push_back({2,2}):{2,2} 会先隐式转换为 AA 对象(因为 AA 的构造函数未被 explicit 修饰),然后 push_back 将这个转换后的 AA 对象拷贝到 vector 末尾。
  • v.emplace_back(1,1):传入 AA 构造函数的参数 1,1 时,emplace_back 会直接在 vector 末尾原地调用 AA(int, int) 构造函数,创建新的 AA 对象,无需提前构造临时 AA 对象。

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

在这里插入图片描述
在这里插入图片描述
代码语言:javascript
复制
//且不支持交叉使用
//emplace_back的函数参数是个模板,推不出来
v.emplace_back({ 1,1 });
v.push_back(2,2);

四、vector接口使用的完整源码

代码语言:javascript
复制
#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;
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-11-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、vector的介绍及使用
    • 1.1 vector的介绍
  • 二、vector的使用
    • 2.1 构造
      • 2.1.1 explicit关键字
    • 2.2 流插入流提取
    • 2.3 vector的扩容机制
    • 2.4 resize
    • 2.5 operator[ ]和at
    • 2.6 修改相关的接口
  • 三、vector的隐式类型转换
    • 3.1 push_back和emplace_back的使用差异
  • 四、vector接口使用的完整源码
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档