手写vector vs std::vector:从功能正确到性能达标

目录

手写vector的必要性

代码的骨架

构造函数和析构函数

默认构造

填充构造

迭代器区间模板带来的陷阱

拷贝构造

析构函数

赋值重载

reserve()和resize()

reserve------预留空间

问题1:这个地方能不能用memecpy?

问题2:这里面为什么要记录旧空间的大小,和string的实现差别是什么?

resize()改变大小

push_back和pop_back

迭代器

迭代器的失效问题

insert和迭代器失效问题

[情况1:扩容导致的失效(insert 和 push_back)](#情况1:扩容导致的失效(insert 和 push_back))

情况2:移动元素导致的失效(insert)

earse和迭代器失效的问题


手写vector的必要性

在上文提过,学习C++标准库,只知道他的用法只是在第一层,除此以外还要理解他的内部实现原理,知道内存怎么管理,迭代器何时失效,异常如何保证。更进一步的能根据特定场景定制容器,甚至自己写一个符合 STL 规范的容器。

因此本篇就来重点谈谈第二层------从会用到明理。

代码的骨架

cpp 复制代码
#pragma once
#include<assert.h>
#include<list>
#include<string>

namespace me
{
    template<class T>
    class vector
    {
    public:
        typedef T* iterator;
        typedef const T* const_iterator;
        // ...
    private:
        iterator _start = nullptr;
        iterator _finish = nullptr;
        iterator _end_of_storage = nullptr;
    };
}

**namespace bit:**通过自定义一个命名空间防止和std的vector造成冲突,方便对比。

template<class T>:构建模板(在class上面),T是元素类型,更有通用性,这样T可以是任意类型甚至是vector<T>

typedef T* iterator定义迭代器类型为原生指针。因为vector内存连续,指针天然支持++,--,+n,-n等随机访问操作。

const_iterator是const T*,用于 const 对象,防止修改元素。

**_start , _finish ,_end_of_storage:**vector的核心,描述vector骨架的关键。

  • _start:指向动态数组的第一个元素。

  • _finish:指向当前最后一个有效元素的下一个位置(即 end())。

  • _end_of_storage:指向当前分配的内存块的末尾(即 capacity 的边界)。

这三个指针决定了所有容量的接口。

为什么用三个指针而不是 sizecapacity 变量?

因为指针运算更高效:size = _finish - _start

capacity = _end_of_storage - _start。同时,指针天然支持迭代器操作,begin()end() 直接返回 _start_finish,无需额外转换。

cpp 复制代码
size_t size() const      
{ 
    return _finish - _start; 
}



size_t capacity() const  
{ 
    return _end_of_storage - _start; 
}



bool empty() const       
{ 
    return _start == _finish; 
}

构造函数和析构函数

这里有两种写法,

cpp 复制代码
vector() = default;//C++11


vector() {}


vector<int> v1;      // 调用 vector()
vector<int> v2{};    // 也调用 vector()

默认构造

C++11的写法是让编译器生成默认构造函数的默认实现(所有成员执行默认初始化)

这里更推荐C++的写法,因为会按成员声明顺序执行默认初始化(对于内置类型,=default 不初始化,保持随机值;但对于类类型会调用默认构造)

而vector {}也是什么都不做,两者效果一样,但=default 更明确表达意图

填充构造

cpp 复制代码
vector(size_t n, const T& val = T())
{
    reserve(n);
    for (size_t i = 0; i < n; i++)
    {
        push_back(val);
    }
}

vector(int n, const T& val = T())
{
    reserve(n);
    for (int i = 0; i < n; i++)
    {
        push_back(val);
    }
}


{
    vector<int> v(10, 1);//针对这种类型的构造
}

先看一个疑点const T& val = T(),这里的T()是什么?

本质是构造函数,这里面无论是内置类型还是自定义类型,如果是内置类型,T()默认是T(0),如果是自定义就调用默认构造函数

cpp 复制代码
vector<int> v(10); //这样可以创建 10 个元素,每个都是 0

为什么会有两个版本?

迭代器区间模板带来的陷阱

是为了区分迭代器版本的填充构造。

cpp 复制代码
template <class InputIterator>
vector(InputIterator first, InputIterator last)
{
    while (first != last)
    {
        push_back(*first);
        ++first;
    }
}

这是一个函数模板成员函数------类模板的成员函数还可以继续是函数模板。

当遇到迭代器区间构造的时候,可以让任意类型的迭代器自动匹配。

cpp 复制代码
// 用 vector 的部分区间构造
vector<int> v1 = {1, 2, 3, 4, 5};
vector<int> v2(v1.begin(), v1.begin() + 3);   // {1, 2, 3}

// 用 list 构造 vector
list<int> lt = {10, 20, 30, 40};
vector<int> v3(lt.begin(), lt.end());         // {10, 20, 30, 40}

如果不写vector(int n, const T& val = T())会怎么样?

在没有vector(int n, const T& val = T())的情况,如果是以下的构造,会调用哪个函数?

cpp 复制代码
vector<int> v(10, 1);

答案是:可能会调用迭代器的构造,引用10,1都是int类型,根据匹配程度,10不是size_t,自动识别为模板类型的InputIterator更加匹配,但是如果调用了迭代器的版本,传入整形,*first这行代码,不能对整形值进行解引用,所有会报错。当然编译器也可能会认为有二义性,编译错误。

拷贝构造

cpp 复制代码
vector(const vector<T>& v)
{
    reserve(v.size());
    for (auto& e : v)
    {
        push_back(e);
    }
}

这里很容易理解,v是vector<T>类型的,被const修饰,只能读,不能写,不影响被拷贝的值。

先给拷贝值一个预留的空间的大小,然后通过for的迭代器进行遍历。

析构函数

cpp 复制代码
~vector()
	{
		delete[] _start;
		_start = _finish = _end_of_storage = nullptr;
	}

这个函数很简单,就是直接释放指向的空间,注意的是只需要释放_start就可以了,后面的都是指向的同一个空间。

赋值重载

cpp 复制代码
// 
vector<T>& operator=(const vector<T>& v)
{
    if (this != &v)
    {
        clear();
        reserve(v.size());
        for (auto& e : v)
        {
            push_back(e);
        }
    }
    return *this;
}

//v1=v3
// 写法2
vector<T>& operator=(vector<T> v)
{
    swap(v);
    return *this;
}

写法2更为简便,而且他的时间复杂度是O(1),是传值传参,v调用拷贝构造的时候,v是v3的拷贝,如果拷贝抛异常,v1保持不变。

reserve()和resize()

reserve------预留空间

cpp 复制代码
void reserve(size_t n)
{
    if (n > capacity())
    {
        size_t old_size = size();
        T* tmp = new T[n];
        // memcpy(tmp, _start, old_size * sizeof(T));  //  不能对vector<string>类型深拷贝
        for (size_t i = 0; i < old_size; i++)
        {
            tmp[i] = _start[i];   //  逐个拷贝
        }
        delete[] _start;
        _start = tmp;
        _finish = tmp + old_size;
        _end_of_storage = tmp + n;
    }
}
问题1:这个地方能不能用memecpy?

不能,因为memcpy只能进行浅拷贝,不能深拷贝。

cpp 复制代码
vector<string> v;
v.push_back("1111111111111111111");
v.push_back("world");
v.reserve(100);   // 扩容

vector<string>本质就是vector容器里面加上了string,然后string在细分为_str,_size_capacity,可以看成是容器对容器的包装,所以当我我们使用memcpy的时候,参数都是string类型的,当我们拷贝的时候是整个string,如图

在拷贝的时候,_size和_capacity是进行值拷贝的,这个没有什么问题,但是在拷贝_str的时候,他会把_str里面存放的旧空间的地址拷贝给新空间的_str,这样的话就会造成他们共同有一个地址,当调用string的析构函数的时候就会释放两次,就是所谓的野指针。

所有说memcpy只是复制的是对象的外壳,对于有指针成员的对象会把地址复制,就会让新空间和就空间的指针成员的对象共享一块内存空间,析构的时候就会释放两次,形成野指针。

因此这里面利用了for循环将成员指针变量逐个赋值

问题2:这里面为什么要记录旧空间的大小,和string的实现差别是什么?

错误写法:

cpp 复制代码
void reserve(size_t n)
{
    if (n > capacity())
    {
        T* tmp = new T[n];
        // ... 拷贝数据 ...
        delete[] _start;
        _start = tmp;
        _finish = tmp + size();   // 错误 调用 size(),但 _start 已经是新地址了!
        // 这时 size() = _finish - _start,但 _finish 还没更新,结果是随机的!
        _end_of_storage = tmp + n;
    }
}

在使用size()的时候,由于上段代码delete掉了_start,又因为size()是需要通过_finish来计算的,这个时候,由于_start和_finish不是在同一连续的内存当中的,所以需要先提前记录原来size()的值。

resize()改变大小

cpp 复制代码
void resize(size_t n, T val = T())
{
    if (n < size())
    {
        _finish = _start + n;   // 缩小:直接移动 _finish
    }
    else
    {
        reserve(n);              // 扩大:先确保空间够
        while (_finish < _start + n)
        {
            *_finish = val;      // 填充
            ++_finish;
        }
    }
}
  • 缩小时,_finish 后移,元素被"逻辑删除",但内存不释放

  • 扩大时,用 val 填充新元素

  • 默认值 T():调用 T 的默认构造

这里的T()既可以是内置类型也可以是自定义类型。

push_back和pop_back

push_back()

cpp 复制代码
void push_back(const T& x)
{
    if (_finish == _end_of_storage)
    {
        reserve(capacity() == 0 ? 4 : capacity() * 2);
    }
    *_finish = x;
    ++_finish;
}

pop_back()

cpp 复制代码
void pop_back()
{
    assert(!empty());
    --_finish;
}

迭代器

cpp 复制代码
 iterator begin()
		{
			return _start;
		}

iterator end()
		{
			return _finish;
		}

const_iterator begin() const
		{
			return _start;
		}

const_iterator end() const
		{
			return _finish;
		}

迭代器的失效问题

insert和迭代器失效问题

cpp 复制代码
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;   //  关键:扩容后 pos 失效,重新计算!
    }

    iterator end = _finish - 1;
    while (end >= pos)
    {
        *(end + 1) = *end;
        --end;
    }
    *pos = x;
    ++_finish;

    return pos;
}

情况1:扩容导致的失效(insert 和 push_back)

cpp 复制代码
vector<int> v = {1, 2, 3};
auto it = v.begin();   // it 指向 1

v.push_back(4);        // 如果扩容,it 失效!

// 此时 it 指向的内存可能已经被释放
// 使用 it → 未定义行为

使用push_back()的时候,会调用reserve(),会建立一个新的空间,但是起初的时候迭代器指向的it是原来的空间,这样如果使用it,it所指向的空间会被释放掉。

错误写法:

cpp 复制代码
iterator insert(iterator pos, const T& x)
{
    assert(pos >= _start);
    assert(pos <= _finish);

    // 扩容
    if (_finish == _end_of_storage)
    {
        reserve(capacity() == 0 ? 4 : capacity() * 2);
    }

    iterator end = _finish - 1;
    while (end >= pos)
    {
        *(end + 1) = *end;
        --end;
    }
    *pos = x;
    ++_finish;

    return pos;
}

由于pos是迭代器类型的,他的返回值是_start,_finish的成员变量,由于在调用insert()的时候调用了reserve()开辟新空间,这个时候pos指向原理的空间不存在了,就造成了所有迭代器都失效的问题,所以这里同理这里先记录了原先容器的大小,通过容器的大小记录旧空间pos指向的位置,reserve完了之后,重新计算他的pos,让pos指向新的空间。

cpp 复制代码
 if (_finish == _end_of_storage)
    {
        size_t len = pos - _start;
        reserve(capacity() == 0 ? 4 : capacity() * 2);
        pos = _start + len;   //  关键:扩容后 pos 失效,重新计算!
    }

情况2:移动元素导致的失效(insert)

cpp 复制代码
vector<int> v = {1, 2, 3, 4};
auto it = v.begin() + 1;   // it 指向 2

v.insert(it, 100);         // 插入后,it 还指向原来的位置吗?

//

it=v.insert(it, 100);  //返回新的迭代器


初始状态(容量 8,当前 size=4):
地址:  0x1000    0x1004   0x1008   0x100C   0x1010   0x1014     0x1018   0x101C
内容:  [  1   ] [  2   ] [  3   ] [  4   ] [  未用 ] [  未用 ] [  未用 ] [  未用 ]
        ↑      ↑                    ↑        ↑
      _start   pos                  end       _finish

步骤1:end = _finish - 1 = 0x100C(指向元素4)
while (end >= pos):
  *(end + 1) = *end  → 将4拷贝到0x1010
  --end  → end指向0x1008(元素3)

步骤2:end = 0x1008(指向元素3)
  *(0x100C) = *0x1008  → 将3拷贝到原4的位置(0x100C)
  --end  → end指向0x1004(元素2)

步骤3:end = 0x1004(指向元素2)
  *(0x1008) = *0x1004  → 将2拷贝到原3的位置(0x1008)
  --end  → end指向0x1000(元素1),此时 end < pos,循环退出

步骤4:*pos = x → 在0x1004位置写入100
++_finish → _finish指向0x1014

最终状态:
地址:  0x1000    0x1004   0x1008   0x100C   0x1010   0x1014     0x1018   0x101C
内容:  [  1   ] [  100  ] [  2   ] [  3   ] [  4   ] [  未用 ] [  未用 ] [  未用 ]
        ↑           ↑                                 ↑
      _start        pos                              _finish

通过返回新的迭代器的位置,记录新的位置。

earse和迭代器失效的问题

cpp 复制代码
void erase(iterator pos)
{
    assert(pos >= _start);
    assert(pos < _finish);

    iterator it = pos + 1;
    while (it != end())
    {
        *(it - 1) = *it;
        ++it;
    }
    --_finish;
}


for (auto it = v.begin(); it != v.end(); )
{
    if (*it % 2 == 0)
    {
        it = v.erase(it);   //  erase 返回删除后的下一个位置
    }
    else
    {
        ++it;
    }
}

vector的嵌套问题------vector<vector><int>

cpp 复制代码
vector<int> v(5, 1);
vector<vector<int>> vv(10, v);   // 10 行,每行是 v 的拷贝
vv[2][1] = 2;
// 等价于:vv.operator[](2).operator[](1) = 2;

这里 vv 的构造调用了 vector(size_t n, const T& val),其中 T = vector<int>。所以:

  1. 先构造一个 vector<int> v(5, 1),包含 5 个 1

  2. vv(10, v) 构造了 10 个 vector<int> 对象,每个都是 v深拷贝 (调用 vector<int> 的拷贝构造)。

  3. vv[2][1] 连续两次调用 operator[]

内存布局上,外层 vv 是一个连续数组,存了 10 个 vector<int> 对象;每个内层 vector<int> 又各自在堆上有一块连续空间存 5 个 int行内连续,行间不连续。

以上就是我整个在实现vector的时候需要注意的问题后面,我会继续补充。

相关推荐
wabs6661 小时前
关于图论【A*算法 | 卡码网127.骑士的攻击的思考】
数据结构·算法·图论·卡码网·广搜的改进版
CIO_Alliance1 小时前
AI认知系列(3)| 数据、算法、算力、场景四要素协同
人工智能·算法·ipaas·系统集成·企业cio联盟·企业级ai化转型
m0_547486663 小时前
《数据结构与算法》全套PPT课件2026(中国海洋大学)
数据结构·算法
旖旎夜光3 小时前
LeetCode 904:水果成篮(滑动窗口) —— 题解
数据结构·c++·算法·leetcode·滑动窗口
怪奇云呼军3 小时前
G.711、Opus 和重采样会拖慢识别吗?闪电智能VoiceAgent 的音频入口怎么选
java·人工智能·python·算法·云计算·音视频
乐观勇敢坚强的老彭4 小时前
C++ 竞赛常用算法模板速查表
开发语言·c++·算法
高洁015 小时前
工信部教考中心证书
人工智能·深度学习·算法·机器学习·知识图谱
我变成萤火虫5 小时前
河南萌新联赛2026第(三)场:郑州轻工业大学
数据结构·c++·算法·贪心算法·stl·深度优先·哈希算法
Dr.kangder5 小时前
嵌入式面试总结(十九)——内存泄露
单片机·算法·面试·职场和发展·架构·硬件架构