一、回顾C++ 模版

1、什么是模版

回顾函数重载

#include <iostream>
using namespace std;

void swap(int& x,int& y)
{
	int tmp = x;
	x = y;
	y = tmp;
	cout << "x = " << x << " y = " << y << endl;
}

void swap(float& x,float& y)
{
	float tmp = x;
	x = y;
	y = tmp;
	cout << "x = " << x << " y = " << y << endl;
}

int main()
{
	int x1 = 2,y1 = 3;
	swap(x1,y1);

	float x2 = 2.01,y2 = 3.01;
	swap(x2,y2);

	return 0;
}

这段代码展示了 C++ 中的函数重载特性:我们定义了两个swap函数,分别用于交换int类型和float类型的变量。
当我们在main函数中传入int类型的x1和y1时,编译器会自动匹配到参数为int&的swap函数;
而传入float类型的x2和y2时,则会匹配到参数为float&的版本,从而实现了不同类型变量的交换操作。
这里我们使用了 C++中函数重载的特性

程序的输出结果为:
x1 = 3 y1 = 2
x2 = 3.01 y2 = 2.01

不过,这种方式存在一个明显的问题:
如果我们还需要交换double、char等其他类型的变量,就必须手动编写更多的swap函数重载版本,这会导致大量重复代码。
为了解决这个问题,C++ 引入了函数模板机制。通过函数模板,我们可以只编写一份通用的函数逻辑,让编译器根据实际使用的参数类型自动生成相应的函数版本,从而大幅减少重复劳动,提高代码的可维护性。

2、template 函数模版

#include <iostream>
using namespace std;

template<typename T>
void swap(T& x,T& y)
{
	T tmp = x;
	x = y;
	y = tmp;

	cout << "x = " << x << " y = " << y << endl;
}
// 这里我们定义了一个函数模版swap
// 可以看出函数模版的返回值是void
// 函数有2个形参,分别为为T& x ,T& y
// 而 T 是形参的类型,由 template<typename T>中的T定义
// 在实例化后,编译器会将T转换为具体的内置类型或者自定义类型

int main()
{
	int x1 = 2,y1 = 3;
	swap(x1,y1);

	float x2 = 2.01,y2 = 3.01;
	swap(x2,y2);

	return 0;
}

在编译器编译后会根据具体的实参,生成同实参类型一直的形参swap函数

3、template 类模版

1、这段程序演示了一个存储int类型数据的vector类型

class A
{
public:
	vector() { /* 初始化逻辑 */ }
    void push_back(int value) { /* 插入int类型逻辑 */ }
    ~vector() { /* 析构 */ }
private:
    int* data;  // 存储int类型的数据
    int size;
    int capacity;
}

从这个类的private可以看出,他的data只能是int*类型的,换句话说,这个vector只能存储int类型的数据。
那么当我们需要一个新的存储float或者char类型的栈时,我们就需要重新写一个类,无法复用当前存储int类型的类。
相当之麻烦,那么,我们能像 template那样直接让编译器像生成函数重载那样,一次编写,多类型复用?
答案是当然可以。

2、这段程序演示了类模版如何使用,使用类模版解决了一次编程,多类型复用的问题,是泛型编程的核心机制,也是C++中大量容器,例如map、vector能够支持任意类型的底层原因。

template<typename T>
class A
{
public:
	vector() { /* 初始化逻辑 */ }
    void push_back(T value) { /* 插入任意类型的逻辑 */ }
    ~vector() { /* 析构逻辑 */ }
private:
    T* data;  // 存储任意类型的数据
    int size;
    int capacity;
}

A<int> intContainer;       // 编译器生成存储int的版本
A<float> floatContainer;   // 编译器生成存储float的版本
A<string> strContainer;    // 编译器生成存储string的版本

二、vector使用基础

1、什么是vector?

在 C++ 中,vector 是标准模板库(STL)提供的动态数组容器,属于类模板,定义在 头文件中。它的核心特点是自动管理内存(可动态扩容),支持随机访问,并且能存储任意类型的数据。
在之前我们学了string,vector的底层与string类似,一个指针(指向动态分配的数组首地址,存储实际元素。)、一个size(当前已存储的元素个数(vector::size()获取))、一个capacity(当前内存空间可容纳的最大元素个数(vector::capacity()获取))。
区别在与,vector引入了类模版,在使用时,必须先指定显示指定模版的类型,如vector、vector),支持任意类型(包括自定义类型)。而string只存储char类型数据。

vector与array的区别?

  1. 本质与定义
    std::array(C++11 引入)
    是固定大小的数组容器,定义在 头文件中,属于模板类。
    声明时必须指定固定大小,例如 std::array<int, 5> 表示存储 5 个 int 的数组。
    本质是对 C 风格数组(如 int arr[5])的封装,内存大小在编译时确定。

std::vector
是动态大小的数组容器,定义在 头文件中,属于模板类。
声明时无需指定大小(或指定初始大小),例如 std::vector,大小可在运行时动态变化。

  1. 内存管理
    std::array
    内存大小固定,编译时分配(通常在栈上,除非用 new 动态分配)。
    无法扩容或缩容,大小从创建到销毁保持不变。
    不自动管理内存,超出范围访问会导致未定义行为(与 C 数组类似)。

std::vector
内存动态分配(通常在堆上),大小可随元素插入 / 删除自动调整。
当元素数量超过当前容量(capacity)时,会自动扩容(重新分配更大内存、拷贝元素、释放旧内存)。
自动管理内存,析构时会释放所有分配的内存,避免内存泄漏。

2、vector的构造函数

在这里插入图片描述
这里是cplusplus中vector构造函数的截图
在这里插入图片描述
这里是vector构造函数的形参声明,很多都是typedef过的,这里能看到原始的类型

1、默认构造函数

explicit vector (const allocator_type& alloc = allocator_type());

功能:创建一个空的 vector,内部没有元素,容量(capacity)初始为 0 。
它使用默认的内存分配器(allocator_type)来管理内存,allocator_type 默认为 std::allocator,T 是 vector 存储元素的类型。
使用:内存分配器我们暂且不表,只看具体使用

vector<int> v1;

创建一个新的vector,名字叫做v1,其中没有存储任何元素。
构造函数形参中 const allocator_type& alloc 这部分是为了指定容器使用的内存分配器,并且提供了默认值allocator_type()。
例如在本例子中,std::vector 在构造时没有传入分配器相关参数,它会自动使用默认的 std::allocator 来管理内存,负责分配和释放存储 int 类型数据的内存,以及对象的构造和析构。

2、填充构造函数

explicit vector (size_type n);
vector (size_type n, const value_type& val,
        const allocator_type& alloc = allocator_type());

功能:
第一个重载形式创建一个 vector,包含 n 个默认初始化的元素。
例如,对于 std::vector v1(n),这 n 个元素都初始化为 0 ;
对于自定义类型,会调用其默认构造函数进行初始化。
第二个重载形式创建一个 vector,包含 n 个值为 val 的元素。
使用:

第一种重载形式:

std::vector<int> nums1(5); // 创建包含 5 个 0 的 vector

第二种重载形式

std::vector<int> nums2(5,10); // 创建包含5个10的vector

第二种重载形式(自定义类型)

std::vector<std::string> strs(3, "hello"); // 创建包含 3 个 "hello" 的 vector

3、范围构造函数

template <class InputIterator>
vector (InputIterator first, InputIterator last,
        const allocator_type& alloc = allocator_type());

这个函数由2部分组成:
第一部分是模版参数 template
第二部分是函数定义,先看第一、二个形参,InputIterator first, InputIterator last,这是左闭右开的迭代器区间
第三个形参是内存分配器
可以理解为,将内存分配器中的数据,依次拷贝到迭代器区间中去。

功能:使用迭代器范围 [first, last) 内的元素来初始化 vector。
它会将迭代器范围内的元素按顺序拷贝到新创建的 vector 中。
InputIterator 可以是任何满足输入迭代器要求的类型,这使得 vector 可以从其他容器(如 std::list、std::array 等)或者数组中初始化。

#include <iostream>
#include <vector>

int main()
{
	std::vector<int> v1(5,20); // 使用填充构造函数在v1中填充5个20
	std::vector<int> v2(v1.begin(),v1.end()); // 使用v1的迭代器去构造 v2 vector

	for(auto e:v2) // 使用范围for遍历vector
	{
		std:: cout << e << " ";
	}
	cout << endl;

	return 0;
}

可以直接点击运行程序查看运行结果为:
20 20 20 20 20

4、拷贝构造函数

vector (const vector& x);
vector (const vector& x, const allocator_type& alloc);

功能:
第一个重载形式是传统的拷贝构造函数,用于创建一个新的 vector,它是参数 x 的副本,新 vector 的元素和容量都与 x 相同,元素会被逐个拷贝。
第二个重载形式允许指定一个内存分配器,用于控制新 vector 的内存分配。
使用:我们只看第一个重载形式,第二个暂且不表

#include <iostream>
#include <vector>

int main()
{
	std::vector<char> v1(5,'a'); // 使用填充构造函数在v1中填充5个字符'a'
	std::vector<char> v2(v1); // 使用拷贝构造去构造v2 vector

	for(auto e:v2) // 使用范围for遍历vector
	{
		std:: cout << e << " ";
	}
	cout << endl;

	return 0;
}

可以直接点击运行程序查看运行结果为:
a a a a a

5、初始化列表(initializer_list)构造函数

注意:初始化列表是在C++11中引进的特性

1、使用方法
vector (initializer_list<value_type> il,
        const allocator_type& alloc = allocator_type());

功能:使用初始化列表 il 中的元素来初始化 vector。
初始化列表是 C++11 引入的特性,它允许使用花括号 {} 来初始化对象,提供了一种更直观和方便的初始化方式。
这样使用就和C语言的极为类似了

#include <iostream>
#include <vector>

int main()
{
	std::vector<char> v1({'a','b','c','d','e','f','g'}); 
	// v1 使用了 直接初始化:std::vector<char> v1({'a','b',...}),直接通过构造函数接收初始化列表
	// 注意这里不是填充构造函数
	// 小括号()里面,是用{}括起来的初始化列表
	
	for(auto e:v1) // 使用范围for遍历vector
	{
		std:: cout << e << " ";
	}
	cout << endl;

	// 但是在实践中更容易这么写
	std::vector<char> v2 = {'0','a','b','c','d','e','f','g'}; 
	// v2 使用了 拷贝初始化:std::vector<char> v2 = {'0','a',...},通过赋值语法接收初始化列表
	// 直接省略了小括号(),使用等号对{}中的初始化列表进行赋值
	// 这样更符合C语言的风格,使用起来更为简单
	
	for(auto e:v2) // 使用范围for遍历vector
	{
		std:: cout << e << " ";
	}
	cout << endl;
	return 0;
}

运行结果:
a b c d e f g
0 a b c d e f g

这种初始化方式极大简化了容器的初始化过程,尤其在已知初始元素的场景下,比传统的 push_back 逐个添加元素更高效(避免了多次可能的内存扩容)。

2、深入理解初始化列表
#include <iostream>
#include <vector>
#include <initializer_list>
// 必须添加initializer_list头文件,否则运行报错
// 运行结果:
// 无法推导il1的类型,因为没有包含支持initializer_list的头文件

using namespace std;

int main()
{
	auto il1 = {10,20,30,40,50};
	cout << typeid(il1).name() << endl;
	// 输出结果:class std::initializer_list<int>
	// 是一个初始化列表类
	// 注意这个初始化列表类,一样是有迭代器的

	cout << "il1.begin() = " << il1.begin() << endl;
	cout << "il1.end() = " << il1.end() << endl;
	//  il1.begin() = 00000000
	//  il1.end() = 00000014
	//  从这里可以看出,初始化列表的底层迭代器是原生指针

	cout << "*(il1.begin()) = " << *(il1.begin()) << endl; 
	cout << "*(il1.end()) = " << *(il1.end()) << endl; 
	// *(il1.begin()) = 10
	// *(il1.end()) = 50
	// 这里更证明了,初始化列表的迭代器是原生指针
	
	cout << "il1.size() = " << il1.size() << endl;
	// il1.size() = 5
	
	// 使用范围for遍历初始化列表
	for(auto e : il1)
	{
		cout << e << " ";
	}
	cout << endl;
	
	return 0;
}

1、在使用初始化列表之前必须包含初始化列表的头文件<initializer_list>。
2、必须使用auto关键字去制定初始化列表的类型,不能显式的表明初始化列表的类型。
如果这么写:int il1 = {10,20,30,40,50};就变成数组了,不是初始化列表。
3、初始化列表的迭代器是原生指针,这个是把初始化列表项vector赋值的基础。
4、初始化列表也有size函数,可以返回列表的有效数字个数。

#include <iostream>
#include <vector>
// 注意此处未包含 initializer_list 头文件
using namespace std;

int main()
{
	vector<int> v2({1,2,3,4,5})
	return 0;
}

1、这是个C++11的语法,我们来看底层是怎么实现的。
2、先看小括号()里的,临时对象{1,2,3,4,5}会被编译器识别为一个initializer_list初始化列表。
3、这个初始化列表中有相应的迭代器(begin()、end()),还有有效元素数量
4、构造函数会先获取 initializer_list 的元素数量(这里是 5 个)
调用内部的内存分配器(通常是 std::allocator)分配能容纳 5 个 int 的连续内存空间
将 initializer_list 中的元素逐一拷贝(对于 int 是直接复制)到新分配的内存中
5、初始化完成后,initializer_list 临时对象会被销毁
但它所指向的数组数据已经被复制到 vector 的内存中,所以不会影响 v1 的内容
这种初始化方式是 C++11 引入的列表初始化(list initialization)特性,底层本质上还是通过 initializer_list 作为中介,完成 vector 的内存分配和数据初始化。

// 但是实践中大多这么写
vector<int> v2 ={ 1,2,3,4,5,6,7,8,9,0 };
 //确实会触发列表初始化(C++11 特性),其过程是:
//编译器将{ 1,2,3,...0 }解析为initializer_list<int>类型的临时对象
//调用 vector 的initializer_list构造函数(而非单参数构造函数)
//直接构造 v2 对象,不会产生中间 vector 对象(这是 C++ 的拷贝消除优化)
//关键点:
//这不是 "单参数隐式类型转换",而是专门的列表初始化规则
//初始化器列表{}会被优先匹配到initializer_list构造函数
//编译器会执行拷贝消除(copy elision),直接在 v2 的内存位置构造对象,避免临时对象的创建和销毁
//所以更准确的说法是:初始化列表会被转换为initializer_list对象,然后 vector 通过这个对象直接构造 v2,整个过程经过优化后不会产生额外的临时 vector 对象。

3、vector的3种遍历方式

注意:vector没有 << 运算符重载,必须使用这3种遍历方式

1、范围for遍历

#include <iostream>
#include <vector>

int main()
{
	std::vector<int> v1 = {1,2,3,4,5,6,7,8,9}; // 使用初始化列表填充vector

	for(auto e:v1) // 使用范围for遍历vector
	{
		std:: cout << e << " ";
	}
	cout << endl;

	return 0;
}

运行结果:
1 2 3 4 5 6 7 8 9

2、迭代器区间遍历

#include <iostream>
#include <vector>

int main()
{
	std::vector<int> v1 = {1,2,3,4,5,6,7,8,9}; // 使用初始化列表填充vector

	std::vector<int>::iterator it1 = v1.begin();
	
	while(it1 != v1.end())
	{
		std:: cout << *it1 << " ";
		it1++;
	}
	cout << endl;

	return 0;
}

运行结果:
1 2 3 4 5 6 7 8 9

3、[]方括号遍历

#include <iostream>
#include <vector>

int main()
{
	std::vector<int> v1 = {1,2,3,4,5,6,7,8,9}; // 使用初始化列表填充vector
	
	for(int i = 0; i < v1.size();i++) // size是有效容量,始终比下标多一位
	{
		cout << v1[i] << " ";
	}
	cout << endl;

	return 0;
}

运行结果:
1 2 3 4 5 6 7 8 9

三、vector扩容-增-删-查

一个简易的vector类代码示意程序

template<typename T>
class vector
{
public:
	vector()
	{/"构造函数"/}
	~vector()
	{/"析构函数"/}
	vector(const vector<T>& x)
	{/"拷贝构造"/}
	void push_back(T value)
	{/"尾插函数"/}
private:
	T* data = nullptr;
	int size = 0;
	int capacity = 0;
}

T* data(指向动态分配的数组首地址,存储实际元素)
size(当前已存储的元素个数(vector::size()获取))
capacity(当前内存空间可容纳的最大元素个数(vector::capacity()获取))

1、vector底层动态数组_capacity和_size

1、_capacity的增长趋势

#include <iostream>
#include <vector>
using namespace std

int main()
{
	vector<int> v1;
	old_capacity = v1.capacity();
	cout << "old capacity = " << old_capacity  << endl;

	for(int i = 0;i < 100;i++)
	{
		v1.push_back(i); // 使用尾插向数组插入数据
		if(v1.capacity() != old_capacity ) // 每次插入数据后都检查下是否满足扩容条件
		{
			cout << v1.capacity()  << endl;
			old_capacity = v1.capacity();
		}
	}
	return 0;
}

old = 0
1
2
3
4
6
9
13
19
28
42
63
94
141
在VS中可以看出基本是1.5倍扩容
在g++中基本看出的2倍扩容

2、resize()函数调整容器元素数量

在这里插入图片描述
这个函数有2个重载形式,
第一个形式 将容器大小调整为n
行为:
若 new_size 大于当前大小,新增元素使用默认构造函数初始化(对于基本类型为 0)
若 new_size 小于当前大小,删除末尾多余的元素

第二个形式 将容器调整到n,并用指定值初始化新增元素
为:
若 new_size 大于当前大小,新增元素都被初始化为 value
若 new_size 小于当前大小,行为与第一个重载相同

注意与string一致:当使用 resize() 缩小 vector 时,容量(capacity)不会改变,只会改变元素数量(size)。

1、内置类型的vector进行reszie
#include <iostream>
#include <vector>
using std::cout;
using std::endl;

int main()
{
	std::vector<int> nums1 = {1,2,3};
	// 定义了一个存储3个元素 int类型的数组

	nums1.resize(5);
	// 将nums1大小调整到 5,内容为:1,2,3,0,0
	// 这是resize重载的第一种形式
	
	nums1.resize(8,88);
	// 将nums1大小调整到8,大于当前大小的元素使用 88 进行填充
	// 内容为:1,2,3,0,0,88,88,88
	// 这是第二种重载形式
	
	cout << "nums1.capacity() = " << nums1.capacity() << endl;
	
	// 缩小vector的大小
	nums1.resize(4);
	// 将nums1大小调整到4,删除末尾多余的元素
	// 内容为:1,2,3,0
	// 无论第一种还是第二种只要小于当前元素个数,都是用第一种重载形式

	cout << "nums1.size() = " << nums1.size() << endl;
	for(auto e : nums1)
	{
		cout << e << " ";
	}
	cout << endl;
	
	cout << "nums1.capacity() = " << nums1.capacity() << endl;
	return 0;
}

运行结果:
nums1.capacity() = 8
nums1.size() = 4
1 2 3 0
nums1.capacity() = 8
从这里可以看出,虽然size减少了,但是capacity没有变化

2、自定义类型的vector进行reszie

再看如何使用resize函数

#include <iostream>
#include <vector>
#include <string>
using std::cout;
using std::endl;
using std::vector;
using std::string;

int main()
{
	vector<string> strs = {"hello"," world"};
	// 定义一个vector对象里面存储了两个string对象
	// 一个string对象存储了“hello”,一个string对象存储了“world”

	// 第一个重载:使用默认构造的空字符串
	strs.resize(4);
	cout << "字符vector默认填充:";
	for(const auto s : strs)
	{
		cout << "[" << s << "] ";// 输出: [a] [b] [] []
	}
	cout << endl;
	
	 // 第二个重载:使用指定字符串填充
	 strs.resize(6,"default");
	cout << "字符vector指定字符填充:";
		for(const auto s : strs)
	{
		cout << "[" << s << "] ";// 输出: [a] [b] [] [] [default] [default]
	}	 
	cout << endl;
	
	return 0;
}

运行结果:
字符vector默认填充:[hello] [ world] [ ] [ ]
字符vector指定字符填充:[hello] [ world] [ ] [ ] [default] [default]

2、尾插数据

#include <iostream>
#include <vector> // 必须添加 vector 头文件
using std::cout;
using std::endl;

int main()
{
	std::vector<int> v1;
	// 与string一致,vector也在std命名空间中

	v1.push_back(1); // 向vector末尾插入1
	v1.push_back(2); // 向vector末尾插入2
	v1.push_back(3); // 向vector末尾插入3
	v1.push_back(4); // 向vector末尾插入4
	// 使用push_back插入数据,注意数据要和类模版的类型一致
	// 因为vector的底层是动态顺序表,所以尾插的效率最高

	std::vector<int>::iterator it1 = v1.begin();
	while(it1 != v1.end())
	{
		cout <<  *it1 << " ";
		it1++;
	}
	cout << endl;
	
}

运行结果:
1 2 3 4

3、在任意位置插入数据

在这里插入图片描述
看图中,单参数的insert,函数的第一个参数都是const修饰的迭代器,第二个则是const修饰的变量引用

#include <iostream>
#include <vector>
using std::cout;
using std::endl;

int main()
{
	std::vector<int> v1 = {1,2,3,4,5,6};

	v1.insert(v1.begin(),0); // 在闭区间0的位置插入
	cout << v1[0] << endl;

	v1.insert(v1.begin() + 3,66); // 在下标3的位置插入数据
	cout << v1[3] << endl;

	std::vector<int>::iterator it1 = v1.begin();
	while(it1 != v1.end())
	{
		cout << *it1 << " ";
		it1++;
	} 
	cout <<endl;
	return 0;
}

输出结果:
0
66
0 1 2 66 3 4 5 6

4、查找vector的数据

注意在vector中没有find,使用算法库中的
find函数使用迭代器进行查找
C++中的迭代器都是左闭右开的

#include <iostream>
#include <vector>
#include <algorithm>

using std::cout;
using std::endl;

int main()
{
	std::vector<char> v1 = {'a','b','c','d','e','f','g'};

	// 注意返回的是迭代器,所以使用auto关键字修饰it,比较简便
	auto it1 = find(v1.begin(),v1.end(),'d');
	if(it1 != v1.end())// 若it不等于end,那么就是找到了
		cout <<*it1 << endl;
	else
		cout << "未找到" << endl;
		
 	// 或者这么写,所以还是让编译器自己推导数据类型吧
 	std::vector<char>::iterator it2 = find(v1.begin(),v1.end(),'g');
 	if(it2 != v1.end())// 若it不等于end,那么就是找到了
		cout << *it2 << endl;
	else
		cout << "未找到" << endl;
		
 	std::vector<char>::iterator it3 = find(v1.begin(),v1.end(),'x');
 	if(it3 != v1.end())// 若it不等于end,那么就是找到了
		cout << *it3 << endl;
	else
		cout << "未找到" << endl;
 	
	return 0;
}

运行结果:
d
g
未找到

5、删除指定位置的数据

在这里插入图片描述
函数提供了2种重载:
第一种:删除指定迭代器位置的数据

#include <iostream>
#include <vector>
#include <algorithm>
using std::cout;
using std::endl;

int main()
{
	std::vector<char> v1 = {'a','b','c','d','e','f','g'};
	
	// 查找数据 d 的迭代器位置
	auto it1 = find(v1.begin(),v1.end(),'d');

	// 删除数据 d,并将数据整体左移
	v1.erase(it1);

	std::vector<char>::iterator it2 = v1.begin();
	while(it2 != v1.end())
	{
		cout << *it2 << " ";
		it2++;
	} 	
	cout << endl;
	
	return 0;
}

运行结果:
a b c e f g

第二种:删除指定迭代器区间中的数据

#include <iostream>
#include <vector>
#include <algorithm>
using std::cout;
using std::endl;

int main()
{
	// 错位示范
	// std::vector<char> v1 = {“hello world”}; // 注意不能直接给初始化列表字符串字面量
	
	// 方法 1:用字符串的迭代器范围构造 vector<char>
	string str = "hello world";
	vector<char> vx (str.begin (), str.end ()); // 遍历字符串的每个字符
	
	// 方法2:手动列出每个char元素(单引号)
	vector<char> v1 = {'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'};
	v1.erase(v1.begin(),v1.begin()+5);

	std::vector<char>::iterator it2 = v1.begin();
	while(it2 != v1.end())
	{
		cout << *it2 << " ";
		it2++;
	} 	
	cout << endl;
	
	return 0;
}

运行结果:
w o r l d

四、vector存储自定义类型

1、什么是vector存储自定义类型

#include <iostream>
#include <vector>
#include <string>
using std::cout;
using std::endl;
using std::vector;
using std::string;

int main()
{
	vector<string> strs = {"a","b"};
	// 定义了一个名为 strs 的 vector 容器
	// 这个容器的元素类型是 string(字符串)
    // 容器中包含两个 string 类型的元素:
	// 第一个元素是字符串 "a"
	// 第二个元素是字符串 "b"
	// 这些元素在容器中是线性排列的(顺序存储),可以通过索引 [0]、[1] 依次访问
	// 可以简单理解为:这是一个 "字符串的有序列表",元素之间是线性的先后关系,就像排队一样,"a" 排在第 1 位,"b" 排在第 2 位。
	// 如果用图示表示,它的结构是这样的:
	// strs: [ "a", "b" ]
    //          ↑     ↑
    //         [0]   [1]  (索引)

	for (int i = 0; i < strs.size(); i++) 
	{
    	cout << strs[i] << endl;  // 依次输出 "a" 和 "b"
	}
	return 0;
}

运行结果:
a
b

2、vector存储自定义类型底层结构是怎么样的?

先看vector的底层结构

template<typename T>
class A
{
public:
	vector() { /* 初始化逻辑 */ }
    void push_back(T value) { /* 插入任意类型的逻辑 */ }
    ~vector() { /* 析构逻辑 */ }
private:
    T* data;  // 存储任意类型的数据
    size_t size;
    size_t capacity;
}

vector 自身的底层结构:
存储着一系列 string 对象(而非字符串地址)
这些 string 对象在 vector 内部是连续排列的
vector 自身也管理着自己的 size(元素数量)和 capacity(容量)

而string底层也是循序表

class string
{
public: 	
	string() { /* 构造含糊 */ }
    ~string() { /* 析构逻辑 */ }
private:
	char* _str = nullptr;
	size_t _size = 0;
	size_t _capacity = 0;
}

每个 string 对象的底层结构:
每个 string 对象内部确实包含:
指向字符数据的指针(字符串地址)
字符串的长度(size)
字符串的容量(capacity)
这是 string 类的内部实现(不同编译器可能有细节差异,但大体如此)

可以用示意图表示这种嵌套关系:

vector<string> strs
┌─────────────────────────────────────────────┐
│ vector 自身管理:                             │
│ size = 2, capacity = 2                      │
│ 存储着两个连续的 string 对象:                  │
│  ┌─────────────── ┐  ┌───────────────┐       │
│  │ string 对象 1   │  │ string 对象 2 │         │
│  │ - 地址 → "a"    │  │ - 地址 → "b"  │        │
│  │ - size = 1     │  │ - size = 1    │       │
│  │ - capacity = 1 │  │ - capacity = 1 │     │
│  └───────────────┘   └───────────────┘       │
└─────────────────────────────────────────────┘

vector 存储的是多个独立的 string 对象(而非字符串地址)
每个 string 对象内部才包含字符串地址、size、capacity 等信息
这些 string 对象在 vector 中形成连续的线性排列,共同组成了 vector 的元素集合
这种结构使得:
vector 可以像数组一样高效访问其中的 string 对象
每个 string 对象又能独立管理自己的字符数据,实现动态字符串功能

3、vector存储结构体分析

之前我们存储的都是C++的内置类型,现在我们来存储自定义类型
先存一个结构体

#include <iostream>
#include <vector>
using std::cout;
using std::endl;

struct A
{
public:
	A(int a1,int a2)
	:_a1(a1)
	,_a2(a2)
	{cout << "A(int a1,int a2)" << endl;}
	
	A(const A& x)
	:_a1(x._a1)
	,_a2(x._a2)
	{cout << "A(const A& x)" << endl;}

	~A()
	{cout << "~A()" << endl;}
	
private:
	int _a1;
	int _a2;
}

int main()
{
	std::vector<A> v1;
	// 这一行不会调用结构体 A 的任何构造函数,原因如下:
	// 这行代码的作用是创建一个空的 vector 容器(类型为 vector<A>),它仅仅是初始化容器本身,
	// 并不会创建任何 A 类型的对象

	A aa1 = {1,2};
	// 结构体 调用构造函数 A(int a1,int a2) 生成对象aa1

	v1.push_back(aa1); // 有名对象赋值
	// 编译器直接将结构体对象,拷贝构造 A(const A& aa) 给 V1

	v1.push_back(A(3, 4)); // 匿名对象赋值
	// 结构体 匿名对象先构造 A(int a1, int a2) 为对象
	// 再调用拷贝构造 A(const A & aa) 将结构体赋值给 V1

	v1.push_back({ 5,6 }); // 多参数的隐式类型转换
	// 注意多参数的隐式类型转换必须有对应的构造函数支持
	
	cout << "-------------------" << endl;

	vector<int> v1 = { 10,20,30 };
	v1.push_back(40);
	v1.emplace_back(50);
	// 在这里push_back 和 emplace_back 的使用方法是一样的

	for (const auto& e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	cout << "-------------------" << endl;
	
	v2.emplace_back(aa1);
	// 效率与push_back差不多
	v2.emplace_back(A(10,20));
	// 效率与push_back差不多
	// v2.emplace_back({30,40}); // 只能直接写入结构体,不支持隐式类型转换
	
	v2.emplace_back(30, 40);
	// 由于模版的可变参数 会直接将 30,40构造成对象,效率最高

	vector<A>::iterator it2 = v2.begin();
	//while (it2 != v2.end())
	//{
	//	cout << (*it2)._a1 << ":" << (*it2)._a2 << endl;
	//	++it2;
	//}
	//cout << endl;
	// 这里有问题,会报错
	// error C2679: 二元“<<”: 没有找到接受“A”类型的右操作数的运算符(或没有可接受的转换)
	// 本质上结构体A中有2个成员变量,不知道用哪个
	// 解决方案:1、重新写一个 << 的重载
	//		   2、因为A的共有的,可以用 . 去访问结构体的成员

	// 实践上喜欢这么写
	while (it2 != v2.end())
	{
		cout << it2->_a1 << ":" << it2->_a2 << endl; // 喜欢用地址+ ->去找
		++it2;
	}
	
	// C++11 中 更喜欢这么写
	for (const auto& aa : v2)
	{
		cout << aa._a1 << ":" << aa._a2 << endl;
		// 因为编译器会将范围for转换为迭代器,aa就是*it,所以可以通过 . 进行访问
	}
	
	// C++17 中更喜欢这么写
	for (const auto&[x, y] : v2) // 结构化绑定 , 不加引用会有拷贝
	{
		cout << x << ":" << y << endl;
	}
	// 这是C++17中的语法糖

	return 0;
}

4、vector存储vector

template<typename T>
class A
{
public:
	vector() { /* 初始化逻辑 */ }
    void push_back(T value) { /* 插入任意类型的逻辑 */ }
    ~vector() { /* 析构逻辑 */ }
private:
    T* data;  // 存储任意类型的数据
    size_t size;
    size_t capacity;
}

上面是vector的间接的底层代码。

那我们存储不同类型vector的数据,vector底层发生了什么呢?

vector<vector<int>>

先看第一层vector,首先可以确定的是这个vector内部存储了一个int型数据
那么他的底层应该是这样的

class vector<int>
{
public:

private:
    int* data;  // 存储int类型的数据
    size_t size;
    size_t capacity;
}

即实例化的时候先将模版参数T实例化为int

那么第二层就是将T实例化为vector

class vector<vector<int>>
{
public:

private:
    vector<int>* data;  // 存储vector<int>类型的数据
    size_t size;
    size_t capacity;
}

即先实例化vector类,再实例化出vector<vector>类
注意这里是2个类

Logo

纵情码海钱塘涌,杭州开发者创新动! 属于杭州的开发者社区!致力于为杭州地区的开发者提供学习、合作和成长的机会;同时也为企业交流招聘提供舞台!

更多推荐