【C++学习笔记】【基础】5.vector

🍕阿i索 个人主页
《C语言专栏》 《C++专栏》
《数据结构专栏》 《LaTeX专栏》
《软件配置问题》 《Linux 专栏》
待更新...

前言.

本章介绍C++ 标准库(STL)中的vector容器。


一、vector

1.定义

vector 是C++ 标准模板库 STL 提供的序列容器 ,使用时必须引入头文件 <vector>。它是类模板 ,书写格式为 vector<存储类型>,可以存放任意相同数据类型(内置类型 int、string,自定义结构体、类对象等)。

vector 对外封装了一套完整的增删改查接口,开发者无需手动开辟、释放数组内存,系统会自动处理内存分配、扩容、销毁工作,因此也被称为动态变长数组,用来替代固定长度的静态数组。

2.本质

vector本质是基于堆上连续原生数组封装而成的STL序列容器 ,对外提供统一操作接口、自动管理内存,底层内存布局和普通数组一致,且能自动扩容实现动态变长。底层完全依赖堆上一段连续的内存空间存储所有元素,内存地址从头到尾紧密相连,不存在空隙。 容器内部维护三个关键指针,完整管控这块连续内存:

  1. _start:指向整块连续内存的起始地址,代表数组头部;
  2. _finish:指向最后一个有效元素的后一个位置,_finish - _start 的结果就是 size(),代表当前真实存放元素的数量;
  3. _end_of_storage:指向整块已分配内存空间的末尾边界,_end_of_storage - _start 的结果就是 capacity(),代表容器当前一共能容纳多少元素。

vector学习文档

二、vector的使用

1. vector 三种打印方式

复制代码
// 打印函数,加const引用避免拷贝,只读不修改容器
void Print(const vector<int>& v)
{
	// 方式1:下标遍历,最简单,vector支持随机下标访问
	for (size_t i = 0; i < v.size(); i++)
	{
		cout << v[i] << " ";
	}
	cout << endl;

	// 方式2:范围for循环,简洁,自动取容器每个元素
	//for (auto e : v)
	//{
	//	cout << e << " ";
	//}
	//cout << endl;

	// 方式3:const迭代器遍历,只读容器时推荐
	//vector<int>::const_iterator it = v.begin();
	//while (it != v.end())
	//{
	//	cout << *it << " "; // *解引用,取出迭代器指向的元素
	//	++it;
	//}
	//cout << endl;
}
  1. vector 没有重载<<运算符,无法直接cout << vector,必须手动封装打印;
  2. 形参const vector<int>& v:引用传参节省拷贝开销,const 保证不会修改容器;
  3. v.size():获取容器当前有效元素个数;
  4. begin()指向首元素,end()指向末尾元素下一位 ,区间左闭右开[begin, end)
  5. const 迭代器只能读取数据,不能修改元素。

2. vector 多种构造初始化

复制代码
void test_vector1()
{
	vector<int> v1;                 // 默认无参构造,空容器,size=0
	vector<int> v2(10,1);           // 创建10个值为1的元素
	vector<int> v3(v2.begin(), v2.end()); // 迭代器区间拷贝构造,复制v2全部元素
	string s1("xxxxxx");
	vector<int> v4(s1.begin(), s1.end()); // 可接收其他容器/字符串的迭代器构造
	
	vector<int> v5(v3);             // 拷贝构造,用已有容器直接复制

	// initializer_list列表初始化,C++11及以上支持
	//vector<int> v6({ 1,2,3,4,5 });
	vector<int> v6 = { 1,2,3,4,5 };
	vector<int> v7 = { 1,2,3,4,5,9,0,7,8 };

    //打印
	Print(v2);
	Print(v4);
	Print(v6);
	Print(v7);
}
  1. 迭代器构造通用性极强,只要容器有begin/end就可以拿来初始化 vector;
  2. 列表初始化写法简单,刷题、日常定义容器最常用;
  3. 拷贝构造会完整复制另一个 vector 所有元素,两个容器内存互相独立。

拓展:initializer_list

std::initializer_list<T> 是 C++11 新增的模板类,专门用来包裹大括号{}逗号分隔的一组同类型常量 ; 当代码写{a,b,c}时,编译器会自动隐式生成一个initializer_list临时对象,不需要手动创建。

复制代码
#include <initializer_list>
#include <vector>
int main()
{
    // 编译器自动把 {1,2,3,4} 转换成 std::initializer_list<int> 临时对象
    vector<int> v{1, 2, 3, 4};
    return 0;
}

作用:

作为编译器和容器 / 自定义类之间的统一桥梁,把代码里大括号{}包裹的一组同类型数值自动封装成只读临时对象,让类能统一接收任意数量的初始化值,实现{元素1,元素2...}批量初始化语法。

它底层只存一段数组的首尾指针,不拷贝数据、开销极低,同时保证所有元素类型统一,相比 C 语言变长参数更安全。

3. vector 容量、扩容、reserve 与 resize

复制代码
void test_vector2()
{
	vector<int> v1;
	// reserve(n):预分配内存,只扩容容量capacity,不创建有效元素size
	//v1.reserve(100);
	size_t old = v1.capacity();
	for (size_t i = 0; i < 100;i++)
	{
		v1.push_back(i);
		// 打印每次扩容后的容量
		if (old != v1.capacity())
		{
			cout << v1.capacity() << endl;
			old = v1.capacity();
		}
	}

	vector<int> v2;
	v1.resize(100, 1); // resize修改有效元素数量size
	Print(v2);
}
  1. size():当前存了多少元素;capacity():容器总内存容量;
  2. 扩容规则:VS 编译器 1.5 倍扩容,g++(Linux) 2 倍扩容;扩容会开辟新内存、拷贝数据、释放旧内存,开销大;
  3. reserve(n):仅提前分配空间,不会增加元素,已知数据量时使用,避免多次扩容;
  4. resize(n, val):修改有效元素个数:
    • n > 原 size:新增元素填充 val;
    • n < 原 size:直接截断尾部多余元素。

4. vector 插入、删除

复制代码
void test_vector3()
{
	vector<int> v1 = { 1,2,3,4,5 };

	v1.push_back(6);               // 尾插6,效率最高,无需挪动数据
	Print(v1);

	v1.insert(v1.begin(), 0);      // 头插0,在迭代器位置插入元素
	Print(v1);

	v1.insert(v1.begin()+3, 0);   // 下标3对应位置插入元素
	Print(v1);

	v1.erase(v1.begin());          // 头删,删除迭代器指向元素
	Print(v1);

	v1.erase(v1.begin()+3);          // 删除指定位置元素
	Print(v1);
}
  1. push_back尾插最优;中间、头部 insert/erase 会挪动后方所有元素,效率低;
  2. insert 接收迭代器作为插入位置,begin()+数字等价按下标定位;
  3. erase 删除指定迭代器元素,删除后后面元素向前补齐;
  4. 插入删除操作可能触发扩容,会让原有迭代器全部失效。

5. emplace_back 和 push_back 对比

复制代码
struct AA
{
	int _a1 = 1;
	int _a2 = 1;
	// 构造函数
	AA(int a1 = 1, int a2 = 1)
		:_a1(a1)
		,_a2(a2)
	{ }
};

void test_vector4()
{
	AA aa1 = { 0,0 };
	// 列表初始化结构体vector
	vector<AA> v = { aa1,{1,1},{2,2},{3,3} };//外层{}是initializer_list,内层{}是多参数构造的隐式类型转换


	// 迭代器遍历结构体容器
	auto it = v.begin();
	while (it != v.end())
	{
		cout << it->_a1 << ":" << it->_a2 << endl;//自定义结构体需要访问成员
		++it;
	}
	cout << endl;

	v.push_back(aa1);     // push_back:先构造临时AA,再拷贝进容器
	v.emplace_back(aa1);  // emplace_back:直接在容器内存原地构造对象,无拷贝
	/*emplace_back 的作用:直接在 vector 容器内部已分配好的堆内存上原地构造对象,不需要创建临时对象再拷贝;而 push_back 是先在外部造出完整对象(临时 / 已有对象),再把它拷贝 / 移动到容器里,多一次拷贝开销*/

	//差异:
	v.push_back({2,2});//push_bacl只能传AA对象
	v.emplace_back(1,1); //emplace可以传AA对象,也可以传构造AA对象的参数

	it = v.begin();
	while (it != v.end())
	{
		cout << it->_a1 << ":" << it->_a2 << endl;
		++it;
	}
	cout << endl;
}
  1. 存储自定义结构体时,迭代器用it->_成员名访问内部变量;
  2. push_back:传完整对象,会发生对象拷贝,复杂结构体开销大;
  3. emplace_back:支持直接传入构造参数,原地构造,无拷贝,推荐优先使用;
  4. {1,1}是结构体临时对象,可直接用于列表初始化和 push_back。

6.练习(杨辉三角)

118. 杨辉三角 - 力扣(LeetCode)

C语言:

对于二维数组 int** 可以考虑使用指针数组来动态开辟二维数组:

复制代码
	int** aa = (int**)malloc(sizeof(int*) * numRows);
	for (int i = 0; i < numRows; ++i)
	{
		aa[i] = (int*)malloc(sizeof(int) * (i + 1));
	}
	return aa;

    //后两个参数
    // 给输出参数赋值:外层总行数
    *returnSize = numRows;
    // 分配存储「每行长度」的数组,必须malloc给外部调用者
    *returnColumnSizes = malloc(sizeof(int) * numRows);

returnSize 是外部传进来的int*一级指针,*returnSize 代表修改指针指向的外部变量; 我们把杨辉三角总行数numRows赋值给它,主函数调用完generate后,读取这个变量就能知道一共返回了多少行。

returnColumnSizesint**二级指针,*returnColumnSizes等价于外部的int*行长度数组;malloc(sizeof(int)*numRows):在堆上申请一块数组,用来存每一行有多少个数字 (第 0 行 1 个、第 1 行 2 个...... 第 n 行 n+1 个);把刚分配好的堆数组地址赋值给*returnColumnSizes,让外部调用者拿到这块内存,遍历读取每行长度。

C++:

复制代码
class Solution {
public:
    vector<vector<int>> generate(int numRows) {
        vector<vector<int>> vv;
        //行
        vv.resize(numRows,vector<int>());//开一个数组出来应该有numRows个vector<int>对象
        //列
        for(size_t i=0;i<numRows;i++)
        {
            vv[i].resize(i+1,1);
        }
        //遍历
        for(size_t i=2;i<vv.size();++i)
        {

            for(size_t j=1;j<vv[i].size()-1;++j)
            {
                vv[i][j]=vv[i-1][j]+vv[i-1][j-1];//上一行同下标位置的值和前一个值相加
            }
        }
        return vv;
    }
};

三、vector的模拟实现

vector.h文件

复制代码
#pragma once
#include <cstring>// 提供memcpy、memset等内存拷贝/初始化函数
#include<assert.h>
#include <initializer_list>// 支持{}列表初始化 vector<int> v={1,2,3};
#include<iostream>
//声明和定义在同一个文件
namespace asuo
{
	template<class T>
	class vector
	{
	public:
		// 迭代器重定义:原生指针作为vector迭代器
		using iterator = T*;// 等同于typedef T* iterator;
		using const_iterator = const T*;// 只读迭代器,不能修改容器内元素

		// 获取容器第一个元素迭代器(可读可写)
		iterator begin()
		{
			return _start;
		}
		// 获取末尾后一位迭代器,代表有效数据边界
		iterator end()
		{
			return _finish;
		}
		// const容器调用,只读begin
		const_iterator begin()const
		{
			return _start;
		}
		// const容器调用,只读end
		const_iterator end()const
		{
			return _finish;
		}

		//构造
		//初始化:初始化列表显式写了用初始化列表,没有显式写看缺省值,都没有也要走初始化列表(初始化列表一定要走)
		// 无参构造,三个指针成员默认初始化为nullptr(类内缺省初始化)
		vector()
		{}

		// initializer_list构造,支持{}初始化容器
		vector(std::initializer_list<T> il)
		{
			// 提前开辟对应大小空间,避免多次扩容
			reserve(il.size());
			// 范围for遍历初始化列表,逐个尾插
			for (const auto& e : il)//语法糖
			{
				push_back(e);
			}
		}

		//析构
		~vector()
		{
			// 防止空指针delete崩溃
			if (_start)
			{
				// 释放堆上动态数组空间
				delete[] _start;
				// 三指针置空,防止野指针
				_start = _finish = _end_of_storage = nullptr;
			}
		}

		////传统写法构造:v2(v1)
		////vector(const vector<T>& v)
		////{
		////	reserve(v.capacity());
		////	for (const auto& e: v)//使用新迭代器的引用
		////	{
		////		push_back(e);
		////	}
		////}

		//// v1=v3
		//// v0=v1=v3
		////vector<T>& operator=(const vector<T>& v)
		////{
		////	if (this!= &v)
		////	{
		////		clear();
		////		reserve(v.capacity());
		////		for (const auto& e : v)
		////		{
		////			push_back(e);
		////		}
		////	}
		////	return *this;
		////}

		//n个val构造
		//构造优先会匹配模板
		// size_t版本重载,处理无符号数值传参场景
		vector(size_t n, const T& val = T())
		{
			reserve(n);
			for (size_t i = 0; i < n; i++)
			{
				push_back(val);
			}
		}
		//解决:提供一个重载
		// int版本重载,区分迭代器模板构造,防止歧义匹配
		vector(int n, const T& val = T())
		{
			reserve(n);
			for (int i = 0; i < n; i++)
			{
				push_back(val);
			}
		}

		//函数模板,迭代器不一定是vector迭代器,也可以是其他容器的迭代器
		// 接收任意容器输入迭代器区间 [first, last) 构造vector
		template <class InputIterator>
		vector(InputIterator first, InputIterator last)
		{
			//reserve(last - first);
			// 遍历迭代器区间,逐个尾插元素
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}

		//现代写法:拷贝构造,拷贝其他对象的
		vector(const vector<T>& v)
		{
			// 用迭代器区间构造临时对象,完成深拷贝
			vector<T> tmp(v.begin(), v.end());
			// 交换临时对象与当前对象三指针,资源转移,出函数tmp自动析构释放旧空间
			swap(tmp);
		}

		//v1=v3 拷贝赋值重载(现代交换写法)
		vector<T>& operator=(vector<T>& tmp)
		{
			// 交换双方指针,tmp接收当前对象旧资源,函数结束自动释放
			swap(tmp);
			return *this;
		}

		// 交换两个vector底层三块指针,高效交换容器数据,不拷贝元素
		void swap(vector<T>& v)
		{
			std::swap(_start, v._start);
			std::swap(_finish, v._finish);
			std::swap(_end_of_storage, v._end_of_storage);
		}

		// 清空有效数据,不释放底层容量空间
		void clear()
		{
			_finish = _start;
		}

		// 判断容器是否为空(无有效元素)
		bool empty()const
		{
			return _start == _finish;
		}

		// 扩容函数:开辟更大新空间,迁移旧数据,释放旧空间
		void reserve(size_t n)
		{
			// 仅当目标容量大于现有容量时才扩容
			if (n > capacity())
			{
				// 记录当前有效元素个数
				size_t sz = size();
				// 开辟n大小的T类型数组新空间
				T* tmp = new T[n];//开辟新空间
				if (_start)//_start不为空就把旧空间的内容拷贝出来
				{
					//memcpy(tmp, _start, sizeof(T) * sz);
					// memcpy浅拷贝,自定义类型如string会浅拷贝导致析构双重释放,废弃不用

					for (size_t i = 0; i < sz; i++)
					{
						//tmp[i] = _start[i];//法一:如果是string,调用string的赋值深拷贝
						std::swap(tmp[i], _start[i]);//法二:如果是string,调用string的交换,交换资源指向,效率更高
					}
					// 释放旧动态数组空间
					delete[] _start;
				}
				// 更新三块指针指向新空间
				_start = tmp;
				_finish = _start + sz;
				_end_of_storage = _start + n;
			}
		}

		/*缺省参数一般使用:自变量常量,全局变量/类的静态成员,匿名对象 */
		//这里T()就是使用了匿名对象,创建T类型默认值填充
		// 修改有效元素个数,扩容/缩减size,容量不缩减
		void resize(size_t n, T val = T())
		{
			if (n < size())
			{
				//删除数据:直接前移_finish截断有效数据,不析构元素
				_finish = _start + n;
			}
			else
			{
				//空间不够就扩容到n
				reserve(n);
				//补充新增位置,全部填充默认值val
				while (_finish < _start + n)//再插入数据
				{
					*_finish = val;
					++_finish;
				}
			}
		}

		// 获取容器总容量(最大可存元素数,包含未使用空间)
		size_t capacity()const
		{
			return _end_of_storage - _start;
		}

		// 获取当前有效元素个数
		size_t size()const
		{
			return _finish - _start;
		}

		// 下标访问,可读可写
		T& operator[](size_t i)
		{
			// 断言防止下标越界访问
			assert(i < size());
			return _start[i];
		}

		// const容器下标只读访问
		const T& operator[](size_t i)const
		{
			assert(i < size());
			return _start[i];
		}

		//尾插:在容器末尾新增元素
		void push_back(const T& x)
		{
			//满了:有效数据占满全部容量,需要扩容
			if (_finish == _end_of_storage)
			{
				// 空容器初始分配4个空间;非空扩容2倍
				reserve(capacity() == 0 ? 4 : capacity() * 2);
			}
			// 写入新元素,有效尾指针后移
			*_finish = x;
			++_finish;
		}

		//尾删:删除最后一个有效元素
		void pop_back()
		{
			// 空容器禁止尾删,断言报错
			assert(!empty());
			// 有效尾指针前移,逻辑删除
			--_finish;
		}

		// 在pos迭代器位置插入元素x,返回新元素迭代器
		iterator insert(iterator pos, const T& x)
		{
			// 校验迭代器合法性,不能超出容器边界
			assert(pos >= _start);
			assert(pos <= _finish);
			//扩容
			/*扩容引起的迭代器失效:当_finish == _end_of_storage(容量已满)时,reserve会开辟新堆内存、释放旧内存,原来的 pos 迭代器指向已销毁的旧空间,变成野指针,直接使用会崩溃。*/
			if (_finish == _end_of_storage)
			{
				// ① 先记录pos相对容器起始的偏移量,扩容后重建迭代器
				size_t len = pos - _start;// 
				reserve(capacity() == 0 ? 4 : capacity() * 2);   // 扩容,旧内存释放,原pos失效
				pos = _start + len;  // ② 用偏移量在新内存上重建合法pos迭代器
			}
			//挪动插入:pos及之后元素全部向后挪一位,腾出pos位置
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}
			// 插入目标元素
			*pos = x;
			// 有效元素数量+1
			++_finish;
			return pos;
		}

		// 删除pos位置元素,返回后一个元素迭代器
		iterator erase(iterator pos)
		{
			// 校验迭代器合法,不能是end()
			assert(pos >= _start);
			assert(pos < _finish);
			// pos后所有元素向前覆盖一位,逻辑删除pos元素
			iterator it = pos + 1;
			while (it != _finish)
			{
				*(it - 1) = *it;
				++it;
			}
			// 有效元素数量-1
			--_finish;
			// 返回原pos位置(此时存储原下一个元素)
			return pos;
		}

	private:
		iterator _start = nullptr;           // 动态数组起始指针
		iterator _finish = nullptr;          // 有效数据末尾下一位指针
		iterator _end_of_storage = nullptr;  // 整个容量空间末尾下一位指针
	};
}

test.cpp测试文件

复制代码
#define _CRT_SECURE_NO_WARNINGS

#include"vector.h"
#include<iostream>
using namespace std;

namespace asuo
{
	// 打印vector所有元素,const引用避免拷贝,只读容器
	void Print(const vector<int>& v)
	{
		// 范围for遍历,依赖begin/end迭代器实现
		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;

		//for (size_t i = 0; i < v.size(); i++)
		//{
		//	cout << v[i] << " ";
		//}
		//cout <<endl;
	}

	void test_vector1()
	{
		asuo::vector<int> v;
		// 尾插5个元素
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		v.push_back(5);
		Print(v);

		// 头插0,测试insert扩容迭代器失效处理
		v.insert(v.begin(), 0);
		Print(v);

		// 删除首元素
		v.erase(v.begin());
		Print(v);

		////删除所有偶数
		////迭代器失效:erase后itt失效,不能再被访问,访问结果未定义
		//auto itt = v.begin();
		//while (itt != v.end())
		//{
		//	if (*itt % 2 == 0)
		//	{
		//		v.erase(itt);
		//	}
		//	++itt;
		//}
		//Print(v);

		//解决:
		//删除所有偶数
		//迭代器失效:erase后itt失效,不能再被访问,访问结果未定义,需要重新赋值迭代器
		auto itt = v.begin();
		while (itt != v.end())
		{
			if (*itt % 2 == 0)
			{
				// erase返回有效迭代器,重新接收,修复迭代器失效
				itt = v.erase(itt);
			}
			else
			{
				// 当前元素不删除,迭代器后移
				++itt;
			}

		}
		Print(v);

		auto it = v.begin() + 3;
		//insert后it迭代器失效(底层指向野指针),不能再被使用,需要重新赋值迭代器
		// insert返回新迭代器,覆盖原失效it
		it = v.insert(it, 30);
		Print(v);

		// 缩小size到3,截断末尾元素
		v.resize(3);
		Print(v);
		// 扩大size到23,自动扩容并用默认值填充空位
		v.resize(23);
		Print(v);
	}

	void test_vector2()
	{
		asuo::vector<int> v1;
		v1.push_back(1);
		v1.push_back(2);
		v1.push_back(3);
		v1.push_back(4);
		v1.push_back(5);
		Print(v1);
		// 拷贝构造测试,现代swap写法深拷贝
		asuo::vector<int> v2(v1);
		for (auto e : v2)
		{
			cout << e << " ";
		}
		cout << endl;

		//asuo::vector<int> v3({ 10,20,30,40 });
		// initializer_list列表初始化
		asuo::vector<int> v3 = { 10,20,30,40 };//多参数构造需要用到initializer_list,需要添加头文件
		// 拷贝赋值重载测试
		v1 = v3;
		for (auto e : v1)
		{
			cout << e << " ";
		}
		cout << endl;

		// n个val构造测试
		asuo::vector<int> v4(10, 1);
		for (auto e : v4)
		{
			cout << e << " ";
		}
		cout << endl;
	}


	void test_vector3()
	{
		// 测试自定义类型string,验证reserve中swap拷贝规避浅拷贝问题
		asuo::vector<string> v1;
		v1.push_back("1111111111111");
		v1.push_back("1111111111111");
		v1.push_back("1111111111111");
		v1.push_back("1111111111111");
		v1.push_back("1111111111111");
		for (auto& e : v1)
		{
			cout << e << " ";
		}
		cout << endl;
	}
}


int main()
{
	asuo::test_vector3();

	int i = 0;
	int j = int();
	int k = int(1);
	//C++11提供{}初始化
	int z = {};
	int y = { 1 };
	int l{ 2 };
	return 0;
}

1. 构造函数二义性问题

问题描述

模板迭代器构造 template<InputIterator> vector(first, last)vector(size_t n, const T& val) 发生匹配歧义: 当传入 vector(10, 1)10 是 int,编译器会优先匹配模板,把 10、1 当成两个迭代器,编译报错。

复制代码
// 迭代器模板构造,会匹配两个int入参
template <class InputIterator>
vector(InputIterator first,InputIterator last)
{
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}
解决方案

重载两个版本:

  1. vector(size_t n, const T& val) 无符号数值版本

  2. vector(int n, const T& val) 有符号 int 版本 区分数值传参与迭代器区间传参,消除重载歧义。

    //n个val构造
    //size_t无符号版本
    vector(size_t n, const T& val = T())
    {
    reserve(n);
    for (size_t i = 0; i < n; i++)
    {
    push_back(n);
    }
    }
    //解决歧义:int有符号重载,优先匹配数字参数
    vector(int n, const T& val = T())
    {
    reserve(n);
    for (int i = 0; i < n; i++)
    {
    push_back(n);
    }
    }

2. 拷贝构造 / 拷贝赋值浅拷贝、内存泄漏、双重释放问题

问题描述

传统直接赋值写法会共用底层数组指针,析构时两个对象释放同一块堆内存,触发崩溃;析构旧资源会内存泄漏。

复制代码
////传统写法拷贝构造(浅拷贝)
//vector(const vector<T>& v)
//{
//	reserve(v.capacity());
//	for (const auto& e: v)
//	{
//		push_back(e);
//	}
//}
////传统赋值重载(未处理自赋值、旧内存泄漏)
//vector<T>& operator=(const vector<T>& v)
//{
//	if (this!= &v)
//	{
//		clear();
//		reserve(v.capacity());
//		for (const auto& e : v)
//		{
//			push_back(e);
//		}
//	}
//	return *this;
//}

解决方案:现代 swap 交换法

  1. 拷贝构造 :用源容器迭代器区间构造临时对象 tmp(自动完成深拷贝),调用swap(tmp)交换当前对象与 tmp 的三块底层指针;函数结束 tmp 析构,自动释放当前对象旧空间。

  2. 拷贝赋值:传值接收临时对象 tmp,直接 swap 交换指针,tmp 生命周期结束释放旧资源。 优势:无需手动 clear、手动释放旧空间,代码简洁、异常安全。

    //现代拷贝构造
    vector(const vector& v)
    {
    vector tmp(v.begin(), v.end());
    swap(tmp);
    }
    //拷贝赋值重载(传值构造临时对象,交换指针)
    vector& operator=(vector& tmp)
    {
    swap(tmp);
    return *this;
    }
    //交换底层三指针,O(1)交换资源
    void swap(vector& v)
    {
    std::swap(_start, v._start);
    std::swap(_finish, v._finish);
    std::swap(_end_of_storage, v._end_of_storage);
    }

3. reserve 扩容时 memcpy 浅拷贝,自定义类型(string)双重释放崩溃

问题描述

memcpy 按字节拷贝,string 内部存储堆字符串,浅拷贝后两个 string 指向同一块字符堆内存,析构两次释放同一块内存程序崩溃。

复制代码
//memcpy(tmp, _start, sizeof(T) * sz);
解决方案

放弃 memcpy,循环std::swap(tmp[i], _start[i])迁移元素: 调用自定义类型自身 swap,仅交换内部资源指针,不重复开辟堆内存,规避浅拷贝风险。

复制代码
void reserve(size_t n)
{
	if (n > capacity())
	{
		size_t sz = size();
		T* tmp = new T[n];
		if (_start)
		{
			for (size_t i = 0; i < sz; i++)
			{
				//std::swap仅交换对象内部指针,规避浅拷贝
				std::swap(tmp[i], _start[i]);
			}
			delete[] _start;
		}
		_start = tmp;
		_finish = _start + sz;
		_end_of_storage = _start + n;
	}
}

4.迭代器失效问题

(1)insert 扩容导致迭代器失效(野指针)

问题描述

容量满时 insert 触发 reserve 扩容:开辟新空间、delete 旧数组,原 pos 迭代器指向已释放的旧内存,成为野指针,访问崩溃。

解决方案

扩容前记录pos - _start偏移量,扩容完成后用_start + 偏移量重建合法迭代器 pos,保证插入位置正确。

复制代码
iterator insert(iterator pos, const T& x)
{
	assert(pos >= _start);
	assert(pos <= _finish);
	if (_finish == _end_of_storage)
	{
		//①记录偏移
		size_t len = pos - _start;
		reserve(capacity() == 0 ? 4 : capacity() * 2);
		//②用偏移重建迭代器
		pos = _start + len;
	}
	//元素后移逻辑省略...
	return pos;
}

(2)erase 循环删除元素迭代器失效

问题描述

erase 删除当前元素后,原迭代器立刻失效,直接++itt访问野指针,程序未定义行为(崩溃 / 乱码)。

复制代码
//auto itt = v.begin();
//while (itt != v.end())
//{
//	if (*itt % 2 == 0)
//	{
//		v.erase(itt);
//	}
//	++itt;
//}
解决方案

接收 erase 返回值:itt = v.erase(itt),erase 返回删除位置下一个有效迭代器,不执行 ++;不删除才迭代器后移。

复制代码
auto itt = v.begin();
while (itt != v.end())
{
	if (*itt % 2 == 0)
	{
		//erase返回下一个有效迭代器,覆盖失效迭代器
		itt=v.erase(itt);
	}
	else
	{
		++itt;
	}
}

5. 支持 C++11 {} 列表初始化语法

问题描述

原生 vector 支持vector<int> v = {1,2,3},自实现容器无对应构造无法使用该写法。

解决方案

增加vector(std::initializer_list<T> il)构造函数,遍历 initializer_list 批量 push_back,支持大括号初始化。

复制代码
vector(std::initializer_list<T> il)
{
	reserve(il.size());
	for (const auto& e : il)
	{
		push_back(e);
	}
}
//测试调用
asuo::vector<int> v3 = {10,20,30,40};

6. 容器交换效率低(逐元素拷贝)

问题描述

直接循环拷贝两个容器所有元素交换,数据量大时性能极差。

解决方案

实现swap(vector& v)成员函数,仅交换_start/_finish/_end_of_storage三个指针,O (1) 复杂度,无元素拷贝。

复制代码
void swap(vector<T>& v)
{
	std::swap(_start, v._start);
	std::swap(_finish, v._finish);
	std::swap(_end_of_storage, v._end_of_storage);
}

7. 无参构造指针野指针、析构 delete 空指针崩溃

问题描述

不初始化指针,成员变量随机垃圾值,析构执行delete[] _start触发空指针 / 野指针崩溃。

解决方案

类内缺省初始化 _start = nullptr; _finish = nullptr; _end_of_storage = nullptr;,析构前判断if(_start)再释放空间。

复制代码
private:
	iterator _start=nullptr;
	iterator _finish = nullptr;
	iterator _end_of_storage = nullptr;

//析构增加判空
~vector()
{
	if (_start)
	{
		delete[] _start;
		_start = _finish = _end_of_storage = nullptr;
	}
}

8. resize 两种场景处理(缩小 / 扩大容量)

问题描述

修改有效元素数量需要区分两种场景:缩小 size 截断数据、扩大 size 填充默认值,扩容需自动调用 reserve。

解决方案

分支判断:

  1. n <size ():直接前移_finish 截断有效数据

  2. n > size ():先 reserve 扩容到 n,循环填充 T () 默认值至_finish == _start+n

    void resize(size_t n, T val = T())
    {
    if (n < size())
    {
    //缩小:直接截断有效数据
    _finish = _start + n;
    }
    else
    {
    //扩大:先扩容,再填充默认值
    reserve(n);
    while (_finish < _start + n)
    {
    *_finish = val;
    ++_finish;
    }
    }
    }

9. 范围 for 循环无法遍历自定义 vector

问题描述

C++ 范围 for 底层依赖begin()end()迭代器接口,未实现则不能使用for(auto e : v)

解决方案

提供重载begin()/end()(普通版本 + const 版本),返回首尾迭代器指针,兼容范围 for 与只读 const 容器。

复制代码
using iterator = T*;
using const_iterator = const T*;

iterator begin(){return _start;}
iterator end(){return _finish;}
const_iterator begin()const{return _start;}
const_iterator end()const{return _finish;}

//测试使用
for (auto e : v)
{
	cout << e << " ";
}

10. 迭代器只读 / 读写区分

问题描述

const 修饰 vector 无法修改内部元素,若只提供 T * 迭代器,const 容器也能修改元素,违反 const 语义。

解决方案

定义const_iterator = const T*,重载 const 版本 begin/end,const 容器返回只读迭代器,禁止修改元素。

复制代码
using iterator = T*;
using const_iterator = const T*;

//普通对象返回可修改迭代器
iterator begin(){return _start;}
//const对象返回只读迭代器,禁止修改元素
const_iterator begin()const{return _start;}