Vector 的学习记录

造轮子记:从零开始实现一个简易Vector

一、引言:从一段“优雅”的代码开始

最近在学习 std::vector 的底层机制时,我看到一位大佬写的 Vector 实现,
代码风格极其优雅,几乎能直接当作 STL 的简化版阅读。

大佬代码如下:

#include <algorithm>
#include <iostream>
#include <sstream>
#include <vector>
using namespace std;

template <typename T>
class MyVector {
 private:
  T *_data;
  size_t _size;
  size_t _capacity;
  void check_range(size_t index) {
    if (index >= _size) {
      stringstream ss;
      ss << "out of range, index: " << index << ", size = " << _size;
      throw out_of_range(ss.str());
    }
  }

 public:
  class iterator {
   private:
    T *ptr;

   public:
    using value_type = T;
    using iterator_category = random_access_iterator_tag;
    using difference_type = ptrdiff_t;
    using pointer = T*;
    using reference = T&;
    iterator() {}
    iterator(T *ptr) : ptr(ptr) {}
    T &operator*() const { return *ptr; }
    T *operator->() const { return ptr; }
    iterator &operator++() {
      ptr++;
      return *this;
    }
    iterator operator++(int) {
      iterator temp = *this;
      ptr++;
      return temp;
    }
    iterator &operator--() {
      ptr--;
      return *this;
    }
    iterator operator--(int) {
      iterator temp = *this;
      ptr--;
      return temp;
    }
    iterator operator+(int n) const { return iterator(ptr + n); }
    iterator operator-(int n) const { return iterator(ptr - n); }
    int operator-(const iterator &other) const { return ptr - other.ptr; }
    bool operator<(const iterator &other) const { return ptr < other.ptr; }
    bool operator==(const iterator &other) const { return ptr == other.ptr; }
    bool operator!=(const iterator &other) const { return ptr != other.ptr; }
  };
  MyVector() : _data(nullptr), _size(0), _capacity(0) {}
  MyVector(size_t n, const T &init = T())
      : _data(new T[n]), _size(n), _capacity(n) {
    for (size_t i = 0; i < n; i++) _data[i] = init;
  }
  MyVector(const MyVector<T> &other)
      : _data(new T[other.size()]),
        _size(other.size()),
        _capacity(other.size()) {
    for (size_t i = 0; i < other.size(); i++) _data[i] = other[i];
  }
  MyVector(const initializer_list<T> &init)
      : _data(new T[init.size()]), _size(0), _capacity(init.size()) {
    for (T x : init) _data[_size++] = x;
  }
  ~MyVector() { delete[] _data; }
  T &operator[](size_t index) { return _data[index]; }
  const T &operator[](size_t index) const { return _data[index]; }
  T &at(size_t index) {
    check_range(index);
    return _data[index];
  }
  const T &at(size_t index) const {
    check_range(index);
    return _data[index];
  }
  size_t size() const { return _size; }
  size_t capacity() const { return _capacity; }
  void reserve(size_t new_capacity) {
    if (new_capacity > _capacity) {
      _capacity = new_capacity;
      T *new_data = new T[_capacity];
      for (size_t i = 0; i < _size; i++) new_data[i] = _data[i];
      delete[] _data;
      _data = new_data;
    }
  }
  void push_back(const T &elem) {
    if (_size == _capacity) reserve(_capacity == 0 ? 1 : _capacity * 2);
    _data[_size++] = elem;
  }
  void resize(size_t new_size, const T &init = T()) {
    if (new_size > _capacity) reserve(new_size);
    for (size_t i = _size; i < new_size; i++) _data[i] = init;
    _size = new_size;
  }
  void pop_back() {
    if (_size == 0) throw out_of_range("can not pop_back empty vector");
    _size --;
  }
  void clear() { _size = 0; }
  iterator begin() const { return iterator(_data); }
  iterator end() const { return iterator(_data + _size); }

  void insert(const iterator &pos, T x) {
    size_t index = pos - begin();
    if (_size == _capacity) reserve(_capacity == 0 ? 1 : _capacity * 2);
    for (size_t i = _size; i > index; i --) _data[i] = _data[i - 1];
    _data[index] = x;
    _size ++;
  }
  void erase(const iterator &pos) {
    for (size_t i = pos - begin(); i < _size - 1; i ++) {
      _data[i] = _data[i + 1];
    }
    _size --;
  }
  void erase(const iterator &start, const iterator &end) {
    int diff = end - start;
    for (size_t i = start - begin(); i < _size - diff; i ++) {
      _data[i] = _data[i + diff];
    }
    _size -= diff;
  }

  MyVector<T>& operator=(const MyVector<T> &other) {
    if (this != &other) {
      delete[] _data;
      _data = new T[other.size()];
      _size = other.size();
      _capacity = other.capacity();
      for (size_t i = 0; i < _size; i ++) _data[i] = other[i];
    }
    return *this;
  }
  T &front() { return _data[0]; }
  const T&front() const { return _data[0]; }
  T &back() { return _data[_size - 1]; }
  const T&back() const { return _data[_size - 1]; }
  bool empty() const { return _size == 0; }

  template<typename... Args>
  void emplace_back(const Args&... args) {
    if (_size == _capacity) reserve(_capacity == 0 ? 1 : _capacity * 2);
    _data[_size++] = T(args...);
  }
};

int main() {
}

这段代码对 Vector 的实现启发了我:要想真正理解标准库,不如自己动手写一个出来。

于是我开始了一场从零造轮子的实践 —— 手写一个自己的 MyVec,一边模仿学习,一边分析 std::vector 背后的设计。

二、思路

1. 模板 & 初始化

直接按数据类型去写发现有很多重复部分,如下列代码

class MyVec_int{
    int* data;
    size_t _size;
};
class MyVec_double{
    double* data;
    size_t _size;
};
class MyVec_char{
    char* data;
    size_t _size;
};

...

int main(){
    MyVec_int vec_int;
    MyVec_double vec_double;
    MyVec_char vec_char;
    ...
    return 0;
}

这时候就可以引入模板。

模板:一次编写,到处复用。起一个占位符作用,由编译器去帮助生成重复代码。

注意到不同模版下只是数据类型不同,我们可以引入c++中的模版template

此处T为占位符,根据类型灵活调整

template <typename T>
class MyVec{
 private:
    T* _data;         // 指向动态数组首元素的指针
    size_t _size;     // 当前已存储的元素个数,模仿c++官方风格,设计成size_t,无符号longlong类型,足够大
};

2. 构造函数

  • 构造函数 (Constructor): 默认构造、带大小的构造、列表初始化等。
    常见的一些申请构造形式:
// vector(size_t n, T init = T() )
// 有大小 有初始值
vector<int> arr(10);
vector<int> arr(10, 1);

//多维数组 vector(vector<T> &other)
vector<int> a(arr);
//拷贝数组操作
vector<int> a = arr;

// 列表初始化
vector<int> arr = {1, 2, 3, 4, 5};
vector<int> arr{1, 2, 3, 4, 5};

我们去尝试着实现它们

template <typename T>
class MyVec{
    private:
        T* _data;         // 指向动态数组首元素的指针
        size_t _size;     // 当前已存储的元素个数
    public:
        //当构造函数没有复杂逻辑时,只有对变量的赋值时,可以采取以下的简练写法
        MyVec() : _data(nullptr), _size(0) {} //默认构造函数 
        
        // 带大小的构造函数
        MyVec(size_t n, T init = T()) : _data(new T[n]), _size(n) {
            for (size_t i = 0; i < _size; ++i) {
                _data[i] = init;
            }
        }
        
        // 拷贝构造函数
        MyVec(MyVec<T> &other) : _data(new T[other.size()]), _size(other.size()) {
            for (size_t i = 0; i < _size; ++i) {
                _data[i] = other[i];
            }
        }
        
        // 列表初始化的构造函数
        MyVec(initializer_list<T> init) : _data(new T [init.size()]), _size(init.size()) {
            size_t i = 0;
            for(T x : init){
                _data[i++] = x;
            }
        }

};

3. 下标访问

  • operator[]: “实现下标访问,通常不进行边界检查。”
  • at(): “提供边界检查的下标访问,若索引超出范围,抛出std::out_of_range异常。”
//进行重构[]和at
template <typename T>
class MyVec{
    private:
        T* _data;         // 指向动态数组首元素的指针
        size_t _size;     // 当前已存储的元素个数
    public:
        MyVec() : _data(nullptr), _size(0) {}

        // 带大小的构造函数
        MyVec(size_t n, T init = T()) : _data(new T[n]), _size(n) {
            for (size_t i = 0; i < _size; ++i) {
                _data[i] = init;
            }
        }
        
        // 拷贝构造函数
        MyVec(MyVec<T> &other) : _data(new T[other.size()]), _size(other.size()) {
            for (size_t i = 0; i < _size; ++i) {
                _data[i] = other[i];
            }
        }
        
        // 列表初始化的构造函数
        MyVec(initializer_list<T> init) : _data(new T [init.size()]), _size(init.size()) {
            size_t i = 0;
            for(T x : init){
                _data[i++] = x;
            }
        }
        
        // 释放内存
        ~MyVec() { delete[] _data; }

        // &保证成功修改(引用地址)
        T& operator[](size_t index) {
            return _data[index];
        }
        
        // at(),提供边界检查的下标访问
        T& at(size_t index) {
            if(index >= _size){
                stringstream ss;
                ss << "out of range, index: " << index << ", size = " << _size;
                throw out_of_range(ss.str());
            }
            return _data[index];
        }
        
        size_t size() {
            return _size;
        }

};

4. 动态扩容 (Reallocation)

数组是一段内存连续的空间 data[index] -> date + index

那么请思考:
vector是如何做到可以不断的增加元素,依旧保持数组内存连续这一特性的?

指针? 指针指来指去把内存连接起来?指针连起的内存并不连续,通过下标访问,通过指针连接起来,逻辑会非常复杂,并非好的选项。

动态扩容? 概念很精辟,但有些抽象,下面开始探究这个抽象概念,尝试去弄清楚。

std::vector 的容量与扩容
  • size():当前已存储元素个数
  • capacity():底层数组当前分配的总空间
  • size() == capacity() 时,vector 会自动申请更大的连续内存块,并搬移已有元素。
扩容策略
  • 常见实现(libstdc++)采用倍增策略:新容量约为旧容量的 2 倍。
  • 这样做能让插入的均摊时间复杂度为 O(1)
可视化结果展示

在这里插入图片描述

模拟动态扩容时所涉及的一些函数:

int capacity() return _capacity;
void reserve(size_t new_capacity);//预留空间
void push_back(T elem);//内存不足便申请空间

void resize(size_t new_size, T init = T());// 改变size大小, 超出部分用init填充
void pop_back();// size --
void clear();// size = 0

实现:


template <typename T>
class MyVec{
    private:
        T* _data;         // 指向动态数组首元素的指针
        size_t _size;     // 当前已存储的元素个数
        // 加入容量部分
        size_t _capacity; // 当前分配的内存容量

    // 为先前代码加入capacity部分
    public:
        
        MyVec() : _data(nullptr), _size(0), _capacity(0) {}
        
        // 带大小的构造函数
        MyVec(size_t n, T init = T()) : _data(new T[n]), _size(n), _capacity(n) {
            for (size_t i = 0; i < _size; ++i) {
                _data[i] = init;
            }
        }
        
        // 拷贝构造函数
        MyVec(MyVec<T> &other) : _data(new T[other.size()]), _size(other.size()), _capacity(other.size()) {
            for (size_t i = 0; i < _size; ++i) {
                _data[i] = other[i];
            }
        }
        
        // 列表初始化的构造函数
        MyVec(initializer_list<T> init) : _data(new T [init.size()]), _size(init.size()), _capacity(init.size()) {
            size_t i = 0;
            for(T x : init){
                _data[i++] = x;
            }
        }

        // 析构函数释放内存
        ~MyVec() { delete[] _data; }
        
        // &保证成功修改(引用地址)
        T& operator[](size_t index) {
            return _data[index];
        }
        
        // 提供边界检查的下标访问
        T& at(size_t index) {
            if(index >= _size){
                stringstream ss;
                ss << "out of range, index: " << index << ", size = " << _size;
                throw out_of_range(ss.str());
            }
            return _data[index];
        }
        
        size_t size() {
            return _size;
        }
        
        size_t capacity() {
            return _capacity;
        }
        
        void reserve(size_t new_capacity) {
            if(new_capacity > _capacity){
                _capacity = new_capacity;
                T* new_data = new T[_capacity];
                for(size_t i = 0; i < _size; ++i){
                    new_data[i] = std::move(_data[i]);
                }

                delete[] _data;
                _data = new_data;
            }
        }
        // 这里采用了倍增扩容策略,确保 push_back 的 摊还时间复杂度为 O(1)。
        void push_back(T elem) {
            if(_size == _capacity){
                // 扩容处理
                reserve(_capacity == 0 ? 1 : _capacity * 2);
            }
            _data[_size++] = elem;
        }

        void resize(size_t new_size, T init = T()) {
            if(new_size > _size)    reserve(new_size);
            for(size_t i = _size; i < new_size; ++i){
                _data[i] = init;
            }
            _size = new_size;
        }

        void pop_back() {
            if(_size == 0){
                throw out_of_range("pop_back: vector is empty");
            }
            
            --_size;
            

        }

        void clear() {
            _size = 0;
        }
};

5. 常量const

引用传参&

现做出演示:
为方便我们观察情况 为每部分输出相应内容在这里插入图片描述

我们发现调用函数时,MyVec发生了拷贝

为func函数加上&,再次观察
在这里插入图片描述

再次去测试临时变量,发生报错
在这里插入图片描述

报错分析
参数类型默认为左值引用,它只能绑定到左值(有名字、可被再次引用的对象),不能绑定到右值(临时对象、匿名值)。
因此,编译器拒绝将右值 {1,23,4} 传递给左值引用参数。
小结

拿拷贝构造函数做出小结

拷贝构造形式能否接受左值能否接受右值(临时对象)说明
MyVec(MyVec<T>& other)仅左值可绑定
MyVec(const MyVec<T>& other)常用写法
MyVec(MyVec<T>&& other)移动构造函数(C++11)

为func函数加上const,再次观察
在这里插入图片描述

为什么加上const就可以正常调用了?
这是因为 const 左值引用可以绑定到右值。
C++ 标准特意允许这种绑定,以便可以高效地传递临时对象而不发生拷贝。
而临时对象的生命周期会被延长到函数调用结束

tip:
当写成func(MyVec&&)时 会变为右值引用
右值引用下临时变量反而可以正常运行
在这里插入图片描述
在这里插入图片描述

做出表格:

调用形式MyVec<int>&const MyVec<int>&MyVec<int>&&
MyVec<int> v; func(v);
func(MyVec<int>{1,2,3});
func({1,2,3});

综上可分析得出:

  • &可避免大数据产生多次复制
  • const可以实现临时变量的引用

根据对于const的学习,我们可以进一步完善我们的构造函数,运行不会发生改变内部数据的函数也可以根据const去完善

// 拷贝构造函数
        MyVec(const MyVec<T> &other) : _data(new T[other.size()]), _size(other.size()), _capacity(other.size()) {
            for (size_t i = 0; i < _size; ++i) {
                _data[i] = other[i];
            }
        }
        
        // 列表初始化的构造函数
        MyVec(const initializer_list<T> &init) : _data(new T [init.size()]), _size(init.size()), _capacity(init.size()) {
            size_t i = 0;
            for(T x : init){
                _data[i++] = x;
            }
        }

        size_t size() const{
            return _size;
        }

        size_t capacity() const {
            return _capacity;
        }

尝试在函数里输出数据,发现报错
在这里插入图片描述

const 对象无法调用非常量成员函数

在自定义类中,如果某个成员函数没有声明为 const,那么常量对象就无法调用它。

举例:

const MyVec<int> a = {1, 2, 3};
cout << a[0]; // ❌ 报错:不能调用非常量 operator[]

原因在于编译器会将 a 的类型视作 const MyVec<int>*,此时只能调用被标记为 const 的成员函数。

STL 的 std::vector 内部提供了两套下标访问接口:

T& operator[](size_t);
const T& operator[](size_t) const;

这样既能支持非常量访问(读写),也能支持常量访问(只读)。

  • 为什么要两套

    • 非 const 版本:可修改对象内容
    • const 版本:允许常量对象访问元素(只读),保证编译器安全性
  • 对应 STLstd::vector 也是这样做的,保证了左值/右值、常量/非常量对象都能使用

向标准学习,我们也去写两套下标访问接口

// &保证成功修改(引用地址) 
T& operator[](size_t index) { 
    return _data[index]; 
} 
// &保证成功修改(引用地址) 
const T& operator[](size_t index) const { 
    return _data[index]; 
}

同时去更新 at 操作

T& at(size_t index) {
    if(index >= _size){
      stringstream ss;
      ss << "out of range, index: " << index << ", size = " << _size;
      throw out_of_range(ss.str());
    }
    return _data[index];
}

const T& at(size_t index) const {
    if(index >= _size){
      stringstream ss;
      ss << "out of range, index: " << index << ", size = " << _size;
      throw out_of_range(ss.str());
    }
    return _data[index];
}

6. 迭代器

平常写代码时,我们总会利用到迭代器,例如

// 利用迭代器访问 vector 中的所有元素
for(vector<int>::iterator iter = vec.begin(); iter != vec.end(); iter++){
    cout << *iter << endl;
}

// 利用迭代器访问 map 中的所有元素
for(map<int, int>::iterator iter = mp.begin(); iter != mp.end(); iter++){
    cout << iter->first << '' << iter->second << endl;
}

//排序 vector
sort(vec.begin(), vec.end());

// 获取最大元素
vector<int>::iterator iter = max_element(vec.begin(), vec.end());
cout << *iter << endl;

// 寻找 >=target 的第一个元素
vector<int>::iterator iter = lower_bound(vec.begin(), vec.end(), target);
cout << iter - vec.begin() << endl;

当然平常大多数时候我们会写 auto,让程序自动识别迭代器。
那么请思考:
迭代器实质是个什么?
是指针吗?不太确切,严格来说它是一个 行为类似指针的类(或结构),封装了对容器内部元素的访问方式,不同底层结构的容器我们可以用相同的语法去访问

for(auto it = c.begin(); it != c.end(); ++it){}

迭代器的好处是什么?
让整个stl容器非常统一,封装成迭代器可以让容器们用相同方式去访问使用,泛型算法也可以相应被使用

为实现上述例子,我们先去更新 begin() 和 end() 两个迭代器,并重载 ++ , != ,*。让编译器在 iteratror 认识这些符号

同时更新前置++与后置++。了解这个++过程后,我们也明白为什么非基础类型++要尽可能前置。

template <typename T>
class MyVec{
  // 上述内容暂时省略
  class iterator{
    private:
     T* ptr;//底层是个指针,并封装
    public:
     iterator() : ptr(nullptr) {}
     iterator(T* ptr) : ptr(ptr) {}
     
     bool operator!=(const iterator& other){
      return ptr != other.ptr;
     }

     //++iter
     iterator& operator++(){
        ptr++;
        return *this;
     }

     //iter++(empty)
     iterator operator++(int){
        iterator temp = *this;
        ptr++;
        return temp;
     }

     T& operator*() const { return *ptr; }

  };
  iterator begin(){
    return iterator(_data);
  }
  iterator end(){
    return iterator(_data + _size);
  }
  
};

经过更新后就可以利用迭代器去访问元素了
在这里插入图片描述

注: 支持begin(), end(), ++ , != 就可以利用范围循环了

我们去尝试使用算法,这里以 sort 为例, 直接尝试发现出现一堆报错, 报错内容非常长这里就先不放了
大致就是相关运算符缺失

我们去更新算法相关的操作符

template <typename T>
class MyVec{
  // 上述内容暂时省略
  class iterator{
    private:
     T* ptr;//底层是个指针,并封装
    public:
     using value_type = T;// STL算法依赖此类型定义
     iterator() : ptr(nullptr) {}
     iterator(T* ptr) : ptr(ptr) {}

     //++iter
     iterator& operator++(){
        ptr++;
        return *this;
     }

     //iter++(empty)
     iterator operator++(int){
        iterator temp = *this;
        ptr++;
        return temp;
     }

     //--iter
     iterator& operator--(){
        ptr--;
        return *this;
     }

     //iter--(empty)
     iterator operator--(int){
        iterator temp = *this;
        ptr--;
        return temp;
     }

    iterator operator+(int n) const { return iterator(ptr + n); }
    iterator operator-(int n) const { return iterator(ptr - n); }
    iterator& operator+=(int n) { ptr += n; return *this; }
    iterator& operator-=(int n) { ptr -= n; return *this; }

     int operator-(const iterator& other) const {
        return ptr - other.ptr;
     }

    //做比较的相关操作符,比较指针地址
    bool operator==(const iterator& other) const { return ptr == other.ptr; }
    bool operator<(const iterator& other) const { return ptr < other.ptr; }
    bool operator>(const iterator& other) const { return ptr > other.ptr; }
    bool operator<=(const iterator& other) const { return ptr <= other.ptr; }
    bool operator>=(const iterator& other) const { return ptr >= other.ptr; }
    bool operator!=(const iterator& other) const { return ptr != other.ptr; }


     T& operator*() const { return *ptr; }

     T* operator->() const { return ptr; }



  };
  iterator begin(){
    return iterator(_data);
  }
  iterator end(){
    return iterator(_data + _size);
  }
  
};

经过调整 sort 函数可正常运行。
在这里插入图片描述

lower_bound 也可正常运行
在这里插入图片描述

总结:迭代器没什么函数其实,主要是运算符重载,以便仿造指针来实现操作,进一步适应函数。

7.插入删除

void insert(const iterator &pos, const T &elem) {};
void erase(const iterator &pos){};
void erase(const iterator &start, const iterator &end) {}
void insert(const iterator &pos, const T &elem) {
    size_t index = pos - begin();
    if(_size == _capacity)  reserve(_capacity == 0 ? 1 : _capacity * 2);
    for(size_t i = _size; i > index; i--) _data[i] = _data[i - 1];
    _data[index] = elem;
    _size++;
};

在这里插入图片描述

试验下来可正常运行

但可能会存在这样一个问题,执行以下操作为什么会出现 0?

  a.insert(a.begin(), a[a.size() - 1]);
  for(int x : a){
    cout << x << ' ';
  }

在这里插入图片描述

这是一个浅拷贝问题,我们来看以下代码片段:

a.insert(a.begin(), a[a.size() - 1]);

这里发生了以下操作:

  1. a[a.size() - 1]

    • 这里返回的是 a 中最后一个元素的 引用(即 T& 类型),它直接指向数组 _data 中的最后一个元素。如果在其他地方修改这个引用,它会影响 a 数组中的实际数据。
  2. a.insert(a.begin(), a[a.size() - 1])

    • 在执行 insert 函数时,传递的参数是 a[a.size() - 1],这是对数组最后一个元素的引用。
  3. insert 过程中的问题

    • insert 函数会将新的元素插入到容器的开头,它会修改容器的底层数据结构(在底层数组 _data 上进行操作)。同时,为了给新元素腾出位置,原来元素会被向后移动。

    • 问题出在 插入时引用失效。由于插入的元素是引用传递的,而引用绑定的是原数据的位置。当调用 insert 后,底层数组发生了数据移动,原本引用的那块内存位置就可能不再有效。这就导致了 “悬空引用”,并且插入的数据不再是原始数据,而是变成了未定义行为,可能出现不正确的结果,比如输出 0

悬空引用的解释
引用是绑定到内存地址的

a[a.size() - 1] 是一个 引用,这意味着它引用了 a 内部的某个内存位置。例如,假设 a[a.size() - 1] 引用的是数组中的第 n-1 个元素。

T& last = a[a.size() - 1];  // last 是对 a 数组最后一个元素的引用

这个 last 引用会绑定到内存中某个位置——例如地址 0x100。如果我们通过 insert 把数据插入到数组开头,整个数组的内容将会被移动,这意味着原先的 0x100 位置可能已经被移走。此时,原本绑定到 0x100 的引用 last 变得“悬空”了,也就是出现了所谓的悬挂指针。

3.2. 移动数据后引用变得无效

insert 过程中:

  • 首先将数据元素向后移动(为了腾出空间),然后插入新元素。
  • 这就意味着,原本引用的数据已经被移动到了新的内存位置,而引用还指向原先的地址,这样就导致了 悬空引用,这个引用指向了一个已被修改或者已释放的内存位置,从而引发不确定的行为(比如 0)。
解决方案:使用拷贝而不是引用

为了解决这个问题,最简单的方法就是 传值显式拷贝,而不是传递引用。这样,insert 函数就能得到一个数据的副本,而不是指向原始数据的引用。这样即使 insert 修改了底层数组,原始数据也不会受到影响。

故我们可以去掉&,做传值而非传地址。

void insert(const iterator &pos, const T elem) {
    size_t index = pos - begin();
    if(_size == _capacity)  reserve(_capacity == 0 ? 1 : _capacity * 2);
    for(size_t i = _size; i > index; i--) _data[i] = _data[i - 1];
    _data[index] = elem;
    _size++;
};

可以发现可以正常运行了
在这里插入图片描述

接着去编写 erase 函数

void erase (const iterator &pos){
    for(size_t i = pos - begin(); i < _size - 1; i++){
        _data[i] = _data[i + 1];
    }
    _size --;
}

void erase (const iterator &start, const iterator &end){
    int diff = end - start;
    for(size_t i = start - begin(); i < _size - diff; i++){
        _data[i] = _data[i + diff];
    }
    _size -= diff;
}

通过编写 我们也知道了vector erase 和 insert 操作复杂度会是O(n) 的。

8.一些补充

在这里插入图片描述

当运行这样的程序时会出现报错,这是由于浅拷贝所导致的指针悬挂问题。
我们需要去实现深拷贝

1. 拷贝赋值运算符 operator=

MyVec<T>& operator=(const MyVec<T> &other) {
    if (this != &other) {
        delete[] _data;                       // 释放原有内存,防止内存泄漏
        _data = new T[other.size()];          // 为新数据申请空间
        _size = other.size();                 // 更新当前元素个数
        _capacity = other.capacity();         // 更新容量
        for (size_t i = 0; i < _size; i++)   // 逐个拷贝元素
            _data[i] = other[i];
    }
    return *this;                             // 返回自身引用,支持连锁赋值 a = b = c;
}

说明

  • 这个函数是深拷贝赋值,保证两个 MyVector 实例互不干扰。
  • 先判断 this != &other 避免自我赋值,否则会先释放自己的数据而导致错误。
  • 注意:这里每个元素都调用了拷贝构造/赋值操作符,如果元素类型很大或非 POD 类型,可考虑使用移动语义优化。

同时更新一些先前未提及但很常用的函数

1. 访问首尾元素

T &front() { return _data[0]; }
const T& front() const { return _data[0]; }

T &back() { return _data[_size - 1]; }
const T& back() const { return _data[_size - 1]; }

说明

  • front() 返回第一个元素,back() 返回最后一个元素。
  • 提供 非 const 版本(可修改元素)和 const 版本(只读)两种重载,以支持常量对象访问。
  • 使用时需要注意容器是否为空,否则访问会越界(可以在实际实现中加异常检查)。

2. 判断容器是否为空

bool empty() const { return _size == 0; }

说明

  • 简单判断 _size 是否为 0。
  • const 修饰函数保证了常量对象也可以调用。
  • 用于快速判断容器状态,类似 STL 的 std::vector::empty()

3. 可变参数构造元素并插入 emplace_back

template<typename... Args>
void emplace_back(const Args&... args) {
    if (_size == _capacity) 
        reserve(_capacity == 0 ? 1 : _capacity * 2);   // 动态扩容
    _data[_size++] = T(args...);                        // 使用传入参数构造元素
}

说明

  • emplace_back 可以直接在容器内部原地构造对象,避免先创建临时对象再拷贝,提高效率。

  • 使用 可变参数模板 (...Args),支持任意数量和类型的构造参数。

  • 内部逻辑:

    1. 先检查容量是否足够,不够则调用 reserve 扩容。
    2. 使用 T(args...)_data[_size] 位置直接构造对象。
    3. _size++ 更新元素数量。
  • 对比 push_back

    • push_back 需要先构造对象再拷贝进容器。
    • emplace_back 可以直接构造在容器内存中,更高效,特别是复杂类型。

补充部分总结

  1. operator=:安全深拷贝赋值,防止自我赋值问题。
  2. front() / back():方便访问首尾元素,同时支持 const 和非 const 对象。
  3. empty():快速检查容器是否为空。
  4. emplace_back:利用模板和可变参数,实现原地构造对象,减少不必要拷贝,提高性能。

这四个操作是 std::vector 中最基础但高频的操作,理解它们有助于掌握 C++ 容器设计思想:安全性 + 高效性 + 泛型支持

完整代码

根据上述一步步的分析学习,我们可以汇总下各部分知识,得到如下代码

template <typename T> class MyVec {
private:
  T *_data;     // 指向动态数组首元素的指针
  size_t _size; // 当前已存储的元素个数
  // 加入容量部分
  size_t _capacity; // 当前分配的内存容量

  // 为先前代码加入capacity部分
public:
  MyVec() : _data(nullptr), _size(0), _capacity(0) {}

  // 带大小的构造函数
  MyVec(size_t n, T init = T()) : _data(new T[n]), _size(n), _capacity(n) {
    for (size_t i = 0; i < _size; ++i) {
      _data[i] = init;
    }
  }
  // 拷贝构造函数
  MyVec(const MyVec<T> &other)
      : _data(new T[other.size()]), _size(other.size()),
        _capacity(other.size()) {
    for (size_t i = 0; i < _size; ++i) {
      _data[i] = other[i];
    }
  }

  // 列表初始化的构造函数
  MyVec(const initializer_list<T> &init)
      : _data(new T[init.size()]), _size(init.size()), _capacity(init.size()) {
    size_t i = 0;
    for (T x : init) {
      _data[i++] = x;
    }
  }

  size_t size() { return _size; }
  size_t size() const { return _size; }

  size_t capacity() { return _capacity; }

  size_t capacity() const { return _capacity; }
  // 析构函数释放内存
  ~MyVec() { delete[] _data; }

  // &保证成功修改(引用地址)
  T &operator[](size_t index) { return _data[index]; }

  // &保证成功修改(引用地址)
  const T &operator[](size_t index) const { return _data[index]; }
  // 提供边界检查的下标访问
  T &at(size_t index) {
    if (index >= _size) {
      stringstream ss;
      ss << "out of range, index: " << index << ", size = " << _size;
      throw out_of_range(ss.str());
    }
    return _data[index];
  }
  const T &at(size_t index) const {
    if (index >= _size) {
      stringstream ss;
      ss << "out of range, index: " << index << ", size = " << _size;
      throw out_of_range(ss.str());
    }
    return _data[index];
  }

  void reserve(size_t new_capacity) {
    if (new_capacity > _capacity) {
      _capacity = new_capacity;
      T *new_data = new T[_capacity];
      for (size_t i = 0; i < _size; ++i) {
        new_data[i] = std::move(_data[i]);
      }

      delete[] _data;
      _data = new_data;
    }
  }
  // 这里采用了倍增扩容策略,确保 push_back 的 摊还时间复杂度为 O(1)。
  void push_back(T elem) {
    if (_size == _capacity) {
      // 扩容处理
      reserve(_capacity == 0 ? 1 : _capacity * 2);
    }
    _data[_size++] = elem;
  }

  void resize(size_t new_size, T init = T()) {
    if (new_size > _size)
      reserve(new_size);
    for (size_t i = _size; i < new_size; ++i) {
      _data[i] = init;
    }
    _size = new_size;
  }

  void pop_back() {
    if (_size == 0) {
      throw out_of_range("pop_back: vector is empty");
    }

    --_size;
  }

  void clear() { _size = 0; }
  class iterator {
  private:
    T *ptr; // 底层是个指针,并封装
  public:
    using value_type = T; // STL算法依赖此类型定义
    iterator() : ptr(nullptr) {}
    iterator(T *ptr) : ptr(ptr) {}

    //++iter
    iterator &operator++() {
      ptr++;
      return *this;
    }

    // iter++(empty)
    iterator operator++(int) {
      iterator temp = *this;
      ptr++;
      return temp;
    }

    //--iter
    iterator &operator--() {
      ptr--;
      return *this;
    }

    // iter--(empty)
    iterator operator--(int) {
      iterator temp = *this;
      ptr--;
      return temp;
    }

    iterator operator+(int n) const { return iterator(ptr + n); }
    iterator operator-(int n) const { return iterator(ptr - n); }
    iterator &operator+=(int n) {
      ptr += n;
      return *this;
    }
    iterator &operator-=(int n) {
      ptr -= n;
      return *this;
    }

    int operator-(const iterator &other) const { return ptr - other.ptr; }

    // 做比较的相关操作符,比较指针地址
    bool operator==(const iterator &other) const { return ptr == other.ptr; }
    bool operator<(const iterator &other) const { return ptr < other.ptr; }
    bool operator>(const iterator &other) const { return ptr > other.ptr; }
    bool operator<=(const iterator &other) const { return ptr <= other.ptr; }
    bool operator>=(const iterator &other) const { return ptr >= other.ptr; }
    bool operator!=(const iterator &other) const { return ptr != other.ptr; }

    T &operator*() const { return *ptr; }

    T *operator->() const { return ptr; }
  };
  void insert(const iterator &pos, const T elem) {
    size_t index = pos - begin();
    if (_size == _capacity)
      reserve(_capacity == 0 ? 1 : _capacity * 2);
    for (size_t i = _size; i > index; i--)
      _data[i] = _data[i - 1];
    _data[index] = elem;
    _size++;
  };
  void erase(const iterator &pos) {
    for (size_t i = pos - begin(); i < _size - 1; i++) {
      _data[i] = _data[i + 1];
    }
    _size--;
  }

  void erase(const iterator &start, const iterator &end) {
    int diff = end - start;
    for (size_t i = start - begin(); i < _size - diff; i++) {
      _data[i] = _data[i + diff];
    }
    _size -= diff;
  }
  MyVec<T> &operator=(const MyVec<T> &other) {
    if (this != &other) {
      delete[] _data;
      _data = new T[other.size()];
      _size = other.size();
      _capacity = other.capacity();
      for (size_t i = 0; i < _size; i++)
        _data[i] = other[i];
    }
    return *this;
  }
  T &front() { return _data[0]; }
  const T &front() const { return _data[0]; }
  T &back() { return _data[_size - 1]; }
  const T &back() const { return _data[_size - 1]; }
  bool empty() const { return _size == 0; }
  template <typename... Args> void emplace_back(const Args &...args) {
    if (_size == _capacity)
      reserve(_capacity == 0 ? 1 : _capacity * 2);
    _data[_size++] = T(args...);
  }
  iterator begin() { return iterator(_data); }
  iterator end() { return iterator(_data + _size); }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值