C++ vector容器的解析和使用

C++的大部分接口是互通的 详解已经在string类中写过了 这里重复的接口属性就不详细写了

1.Member functions

C++提供的顺序表容器 在使用前建议先系统的学习过数据结构中的顺序表 详情请点

constructor

请添加图片描述
explicit 说明:防止隐式转换(vector v = 5 这是错误的 )
在这里插入图片描述

  1. 默认构造函数(无参构造)
    功能:创建一个空的 vector,不包含任何元素。
std::vector<int> v;  // 创建空的 int 类型 vector
std::cout << v.size();  // 输出 0(无元素)
  1. 填充构造函数(指定大小和初始值)
    功能:创建一个包含 n 个元素的 vector,每个元素的初始值为 val。
std::vector<int> v(3, 10);  // 创建包含3个元素的vector,每个元素为10
// 结果:v = [10, 10, 10]
  1. 范围构造函数(用迭代器区间初始化)
    功能:通过迭代器区间 [first, last) 中的元素初始化 vector,即复制该区间内的所有元素(包含 first 指向的元素,不包含 last 指向的元素)。
int arr[] = {1, 2, 3, 4};
std::vector<int> v(arr, arr + 3);  // 复制arr[0]、arr[1]、arr[2]
// 结果:v = [1, 2, 3]

std::vector<int> v2(vec.begin(), vec.end());  // 复制另一个vector的所有元素
// 结果:v2 = [1, 2, 3]
  1. 拷贝构造函数(复制另一个 vector)
    功能:创建一个新的 vector,并复制另一个 vector x 中的所有元素(深拷贝,两个容器后续修改互不影响)。
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2(v1);  // 拷贝v1的元素
// 结果:v2=[1, 2, 3]

然后C++11中还引入了initializer_list(使用需要包含initializer_list的头文件)

#include<iostream>
#include<vector>
#include<initializer_list> 
int main() {
    // 假设 il 是一个整数类型的初始化列表
    auto il = {1, 2, 3, 4, 5};  // 推断为 initializer_list<int>
    // 输出 il 的类型名(不同编译器可能显示不同,通常为 "initializer_list<int>" 相关形式)
    cout << typeid(il).name()<<endl;
    // 遍历初始化列表中的元素并输出
    for (auto e : il) 
    {
        cout << e << " ";
    }
    cout<<endl;  // 输出:1 2 3 4 5
    // 用初始化列表直接初始化 vector(C++11 及以上支持)
    vector<int> v1({10, 20, 30});  // v1 包含元素 10, 20, 30
    return 0;
}
vector (initializer_list<value_type> il,
const allocator_type& alloc = allocator_type());

vector 的初始化:
vector<int> v1({10,20,30})或者vector<int> v1=({10,20,30}) (构造传参)是 C++11 引入的列表初始化方式,等价于更简洁的 vector<int> v1{10,20,30} 或者vector<int> v1={10,20,30}(走隐式类型转换 构造+拷贝构造 直接优化为构造),直接构造包含指定元素的向量。
注意:initializer_list 中的元素是常量,无法修改(如 vector[0] = 5 错误)。
而且 他的遍历是要通过迭代器的方式 但是放在别的容器中还可以让他像数组一样!
在这里插入图片描述

initializer_list初始化的本质相当于 在范围for中 一直用push_back()插入数据。
在这里插入图片描述

std::vector::operator=

copy (1) vector& operator= (const vector& x);

赋值重载 将新内容分配给容器,替换其当前内容,并相应地修改其大小。
功能说明

  • 作用:把 vector x 中的所有元素复制到当前 vector 中。
  • 特性:这是一个深拷贝操作,赋值后两个 vector 各自拥有独立的元素,修改其中一个不会影响另一个。
  • 行为:赋值前会先清空当前 vector 中的原有元素,再复制 x 的元素。
// vector assignment
#include <iostream>
#include <vector>

int main ()
{
  vector<int> a (3,0);
  vector<int> b (5,0);

  b = a;
  a = vector<int>();

  cout << "Size of a: " << int(a.size()) << '\n';
  cout << "Size of b: " << int(b.size()) << '\n';
  return 0;
}

结果:Size of a: 0 ;Size of b: 3

2.Iterators

迭代器 没什么好说的 但是有一说一 vevtor容器是不支持直接像数组一样遍历 必须使用迭代器请添加图片描述
其接口特性和string 基本一致 详情请转跳

在这里插入图片描述

在这里插入图片描述

3.Capacity

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

vector的容器部分 其效果基本也与string 相似 本质上是并没有太大的区别
详情请转跳

4.Element access

在这里插入图片描述
vector的容器部分 其效果基本也与string 相似 本质上是并没有太大的区别
详情请转跳

5. vector 增删查改

这里面会产生一些不同于string容器的变化 我这里会挑一些重点讲 请添加图片描述

push_back&pop_back

void push_back (const value_type& val);
void push_back (value_type&& val);
vector<int> v1;
    v1.reserve(100);//扩容到100 减少多次扩容
    size_t old=v1.capacity();
    cout<<old<<endl;
    for (size_t i=0; i<100; i++)//尾插
    {
        v1.push_back(i);
        if (v1.capacity()!=old)
        {
            cout<<v1.capacity()<<endl;
            old=v1.capacity();
        }
    }

请添加图片描述

void pop_back();
Delete last element

vector::pop_back() 只会减少 vector 的元素数量(size),不会改变其容量(capacity)。
移除向量中的最后一个元素,实际上会使容器大小减少一个。
这会销毁被移除的元素。 这里就不演示了

insert&&find

  1. 插入单个元素
iterator insert (iterator position, const value_type& val);

string不同 vector没有办法使用pos 而是要使用迭代器
返回值:指向新插入元素的迭代器(方便后续操作)

vector<int> v = {1, 3, 4};
// 在索引1位置插入元素2(通过迭代器 v.begin() + 1 指定位置)
v.insert(v.begin() + 1, 2);
// 结果:v = [1, 2, 3, 4],it 指向新插入的 2
  1. 插入 n 个重复元素
void insert (iterator position, size_type n, const value_type& val);

返回值:void

std::vector<int> v = {1, 4};
// 在开头插入 2 个元素 0
v.insert(v.begin(), 2, 0);
// 结果:v = [0, 0, 1, 4]
  1. 插入迭代器范围元素
template <class InputIterator>
void insert (iterator position, InputIterator first, InputIterator last);

返回值:void

std::vector<int> v1 = {1, 5};
std::vector<int> v2 = {2, 3, 4};
// 在 v1 的索引1位置插入 v2 的所有元素
v1.insert(v1.begin() + 1, v2.begin(), v2.end());
// 结果:v1 = [1, 2, 3, 4, 5]

虽然vector容器里面并没有现成的find 但是在std中是有的

template <class InputIterator, class T>
   InputIterator find (InputIterator first, InputIterator last, const T& val);

返回一个迭代器,指向范围 [first, last) 中第一个与 val 比较相等的元素。如果未找到这样的元素,则函数返回 last。

auto it=find(v1.begin(), v1.end(), 5);//返回的也是迭代器
    if (it!=v1.end())
    {
        v1.insert(it, 50);//在数字5的位置插入50
    }

erase

iterator erase (iterator position);
iterator erase (iterator first, iterator last);
  1. 删除单个元素
vector<int> v = {1, 2, 3, 4};
// 删除索引1处的元素(值为2)
auto it = v.erase(v.begin() + 1);
// 结果:v = [1, 3, 4],it 指向元素3(被删除的下一个元素)
  1. 删除范围内的元素
vector<int> v = {1, 2, 3, 4, 5};
// 删除索引1到3(不包含3)的元素(即2和3)
auto it = v.erase(v.begin() + 1, v.begin() + 3);
// 结果:v = [1, 4, 5],it 指向元素4

emplace_back

这个接口因为目前知识深度的原因 没发讲全 但是也是可以讲一部分

vector<int> v1{10,20,30};//让其像数组一样
    cout<<v1[1]<<endl;//输出 20
    v1.push_back(1);
    v1.emplace_back(1);//这么用可以理解成两者没有区别

push_back和emplace_back的区别

这个是我们自定义的结构体

struct A
{
    A(int a1,int a2)
    :_a1(a1)
    ,_a2(a2)
    {
        cout<<"A(int a1,int a2)"<<endl;
    }
    
    A(const A& aa)
       :_a1(aa._a1)
       ,_a2(aa._a2)
    {
        cout<<"A(const A& aa)"<<endl;
    }
    int _a1;
    int _a2;
};
    vector<A> v2;
    //push_back的三种方法
    //1.有名对象
    A aa1(1,1);
    v2.push_back(aa1);
    //2.匿名对象
    v2.push_back(A(2,2));
    //3.隐式类型转换
    v2.push_back({3,3});
    
    v2.emplace_back(aa1);
    v2.emplace_back(A(2,2));
    //v2.emplace_back({3,3)}; 不能这么用
    //可以这么用
    //传构造A的参数,效率比较高
    v2.emplace_back(3,3);//直接构造

前两种可以理解二者没有区别 最后一种是采用直接的构造效率更高

自定义类的遍历(含结构化绑定知识点)

    //A*
    vector<A>::iterator it2=v2.begin();
    while (it2!=v2.end())
    {
       // cout<<*it2<<" ";这么写是错误的 因为没有重载流插入
        cout<<(*it2)._a1<<":"<<(*it2)._a2<<endl;
        cout<<it2->_a1<<":"<<it2->_a2<<endl;//这么写也可以
        ++it2;
    }
    cout<<endl;
    
    //当然肯定是范围for更香一点 c++11
    for(auto& aa:v2)
    {
        cout<<aa._a1<<" "<<aa._a2<<endl;
    }
    
    //c++17 新出的语法糖 当容器是结构的时候就可以这么用
    auto[x,y]=aa1;//取aa1的成员依次进行赋值
    for(auto[x,y] :v2)//结构化绑定
    {
        cout<<x<<":"<<y<<endl;
    }
    
    for(auto&[x,y] :v2)//不想有拷贝构造函数
    {
        cout<<x<<":"<<y<<endl;
    }

请添加图片描述

6. 例题板块

例题1.只出现一次的数字(^ 的用法)

请添加图片描述

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int x = 0;  // 初始化为0
        for (auto e : nums) {  // 遍历数组中的每个元素
            x ^= e;  // 累积异或运算
        }
        return x;  // 最终结果就是只出现一次的数字
    }
};

核心原理:异或运算的特性

  • 自反性:a ^ a = 0(任何数与自身异或结果为 0)
  • 恒等性:a ^ 0 = a(任何数与 0 异或结果为其本身)
  • 交换律和结合律:a ^ b ^ c = a ^ c ^ b(运算顺序不影响结果)

假设数组为 [2, 3, 2, 4, 4]

x = 0 ^ 2 → 2
x = 2 ^ 3 → 1(二进制 10 ^ 11 = 01)
x = 1 ^ 2 → 3(二进制 01 ^ 10 = 11)
x = 3 ^ 4 → 7(二进制 11 ^ 100 = 111)
x = 7 ^ 4 → 3(二进制 111 ^ 100 = 011)
最后结果就是 3

异或运算的自反性(a ^ a = 0)和恒等性(a ^ 0 = a)在此过程中起关键作用:
数组中出现两次的数字(如 2、4),经过两次异或后会相互抵消(结果为 0)。
最终剩下的数字就是只出现一次的数字(如 3),因为它只被异或了一次。

例题2.杨辉三角(C和C++做法的比较)

在这里插入图片描述

如果用C语言来写的话 你需要动态开辟二纬数组 释放的时候需要讲空间进行分别进行释放
请添加图片描述
如果你想用c写,你需要返回两个你不怎么认识的指针给测试者。
其中 *returnSize是让调用者知道有多少行,然后 *returnColumnSizes是让调用者知道为每一行分配的列数。

int** generate(int numRows, int* returnSize, int** returnColumnSizes) {

    int** aa =(int*) malloc(numRows * sizeof(int*));//为指针数组分配内存,定义行数
    
    //对程序结构没影响,但如果缺少则无法与出题者交互,进而不能通过测试
    *returnSize = numRows;//让调用者知道有多少行
    *returnColumnSizes = (int*)malloc(numRows * sizeof(int));//为每一行分配列数做准备【x,x,x,x,x...】

    for (int i = 0; i < numRows; i++) {
        triangle[i] =(int*) malloc((i+ 1) * sizeof(int));//为特定的行分配内存,定义了该行的具体元素数量
        (*returnColumnSizes)[i] = i + 1; // 为每一行分配列数【1,2,3,4,5...】,让调用者知道

        // 每行的第一个和最后一个元素为 1
        aa[i][0] = 1;
        aa[i][i] = 1;

        // 计算当前行中间的元素
        for (int j = 1; j < i; j++) {
            aa[i][j] = aa[i - 1][j - 1] + aa[i - 1][j];
        }
    }

    return aa; // 返回生成的杨辉三角

}


让我们看看C++是怎么做的

class Solution {
public:
    vector<vector<int>> generate(int numRows) {
        vector<vector<int>> vv;
        vv.resize(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.operator[](i).operator[](j)
               vv[i][j]=vv[i-1][j]+vv[i-1][j-1];
            }
        }
        return vv;
    }
};
vv.resize(numRows, vector<int>())

这个操作将 vv 调整为有 numRows 行,每行是一个空的 vector。注意,vector() 是一个空的 int 类型的向量,表示每一行的初始状态为空
请添加图片描述
里面的vector<int>相当于 一个int*的数组 同时伴随容器的sizecapacity,而外层的vector则是一个包含着一个vector<int>*数组的一个类。

  • 外部 vector (vv): 存储的是指向内部 vector 的指针。
  • 内部 vector (vector): 每个内部的 vector 实际上是一个独立的动态数组。

而这么做的目的也就是为开辟一个动态的二维数组。这也就是STL容器所带来的简便性!

7. vector模拟实现和实现过程中的一些重难点

_start:指向内存空间的起始位置,即第一个元素的地址。
_finish:指向当前已存储元素的下一个位置,即末尾元素的地址 + 1。
_endofstorage:指向已开辟内存空间的末尾位置(已开辟内存的最后一个字节的下一个位置)。

7.1模拟实现完整代码

#include<iostream>
#include <cassert>
using namespace std;

//模版 申明和定义不能分离定义到两个文件.h .cpp
namespace fcy
{
template<class T>
class vector
{
public:
    typedef T* iterator;
    typedef const T* const_iterator;
    
    iterator begin()
    {
        return _start;
    }
    
    iterator end()
    {
        return _finish;
    }
    const_iterator begin() const
    {
        return _start;
    }
    
   const_iterator end() const
    {
        return _finish;
    }
//    vector():
//    _start(nullptr)
//    ,_finish(nullptr)
//    ,_endofstorage(nullptr)
//    {}
    //因为有了缺省值所以还可以
//    vector()
//    {}
    
    //c++11强制编译器生成默认构造
    vector()=default;
    
    //多参数拷贝构造
    vector(initializer_list<T> il)
    {
        reserve(il.size());
        for(auto& e:il)
        {
            push_back(e);
        }
    }
  //迭代器区间初始化 但这么写只能传vector 的iterator
//    vector(iterator start,iterator end)
//    {
//        while (start!=end)
//        {
//            push_back(*start);
//            ++start;
//        }
//    }
    template<class it>
    //函数模版
    //只要类型匹配 我可以用任意类型迭代器来进行初始化
    vector(it first,it last)
    {
        while (first!=last)
        {
            push_back(*first);
            first++;
        }
    }
    //非法间接寻址
//    vector(size_t n,T val=T())
//    {
//        resize(n,val);
//    }
    //改成int就没问题吗? 这只能解决一种情况 所以咱们就多搞两个
    vector(int n,T val=T())
    {
        resize(n,val);
    }

    vector(size_t n,T val=T())
    {
        resize(n,val);
    }
    //v2(v1) 拷贝构造 写法1 这种写法必须要初始化 不然reserve鬼知道会发生什么 迭代器都是随机值 当然也可以用缺省值进行初始化
    vector(const vector<T>& v)
   // _start(nullptr)
    //,_finish(nullptr)
    //,_endofstorage(nullptr)
    {
        reserve(v.capacity());
        for(auto& e:v)
        {
          push_back(e);
        }
    }
    void swap(vector<T>& v)
    {
        std::swap(_start, v._start);
        std::swap(_finish, v._finish);
        std::swap(_endofstorage, v._endofstorage);
    }
    //v1=v7
    vector<T>& operator=(vector<T> v)
    {
        swap(v);
        return *this;
    }
    
    ~vector()
    {
        if(_start)
        {
            delete[] _start;
        }
        _start=_finish=_endofstorage=nullptr;
    }
    
    //string 由_str _size _capacity 组成
    
    
    //扩容
    void reserve(size_t n)
    {
        if(n>capacity())
        {
            size_t old_size=size();//修正1
            T* tmp=new T[n];
            //拷贝旧空间数据到新空间
            if (_start)
            {
               // memcpy(tmp,_start,sizeof(T)*size());
                //string中的指针也会原样赋值 会导致str*指向同一空间
                //然后空间释放时候导致tmp所指向的也一样释放了
                //导致了浅拷贝
                //所以建议用赋值来解决
                for(size_t i=0;i<old_size;i++)
                {
                    tmp[i]=_start[i];
                }
                //int这样的类型,赋值可以完成浅拷贝
                //string这样的类型,赋值重载完成深拷贝
                delete[] _start;
            }
            _start=tmp;
            //_finish=_start+size();
            //此时的size()是有问题的 因为此时
            //size=_finish(原来的)-_start=0(因为初始化时候为0)-_start
            //所以要提前更新 参考修正1
            _finish=_start+old_size;
            _endofstorage=_start+n;
        }
    }
    
    void resize(size_t n,T val =T())//本质就是调用默认构造
    {
      if(n>size())
      {
          reserve(n);
          while (_finish!=_start+n)
          {
              *_finish=val;
              ++_finish;
          }
      }
        else
        {
            _finish=_start+n;
        }
    }
    
    
    
    
    T& operator[](size_t i)
    {
        assert(i<size());
        
        return _start[i];
    }
    
    const T& operator[](size_t i)const
    {
        assert(i<size());
        
        return _start[i];
    }
    void clear()
    {
        _finish=_start;
    }
    
    size_t size() const
    {
        return _finish-_start;
    }
    
    size_t capacity() const
    {
        return  _endofstorage-_start;
    }
    
    //尾插
    void push_back(const T& x)
    {
        if (_finish==_endofstorage)
        {
            reserve(capacity()==0?4:capacity()*2);
        }
        *_finish=x;
        _finish++;
    }
    
    //尾删
    void pop_back()
    {
        assert(!empty());
        --_finish;
    }
    
    bool empty() const
    {
        return  _start==_finish;
    }
    
    iterator insert(iterator pos,const T& x)//不能用& 因为假设传的begin()调用函数iterator begin() 或者说 是表达式的形式 返回的是零时对象 具有常性
    {
        assert(pos>=_start);
        assert(pos<=_finish);
        
        if(_finish==_endofstorage)
        {
            size_t len=pos-_start;
            reserve(capacity()==0?4:capacity()*2);//扩容会引起迭代器失效
            //更新pos
            pos=_start+len;
            //指向新内存中插入的数值的位置,是有效的迭代器
       }
        
        iterator end=_finish-1;
        while(end>=pos)
        {
            *(end+1)=*end;
            --end;
        }
        *pos=x;
        _finish++;
        
        return pos;
    }
    
    iterator erase(iterator pos)
    {
        assert(pos>=_start);
        assert(pos<_finish);
        //挪动数据
        iterator it=pos+1;
        while (it!=_finish)
        {
            *(it-1)=*it;
            it++;
        }
        _finish--;
        
        return pos;//返回指向删除数据的下一个位置
                  //比如删除位置为2 结束后指向位置3
    }
    
    
private:
    iterator _start=nullptr;
    iterator _finish=nullptr;//最后一个数据的下一个
    iterator _endofstorage=nullptr;//已开辟内存的最后一个字节的下一个位置
};
}

7.2迭代器失效问题

问题解析

首先我们需要先了解迭代器是什么?
迭代器的本质:指向内存地址的指针
vector的迭代器本质是原生指针(或行为类似指针的对象),直接指向元素在内存中的地址。例如:
若pos迭代器指向内存地址0x1000(对应元素A),那么*pos就是访问0x1000地址上的数据。
为什么迭代器会失效呢?
迭代器的核心作用是 “指向特定元素”。删除后,pos原本指向的元素A已被移除,0x1000地址上的元素变成了B,pos不再指向 “原来的元素”,语义上已失效。

//insert 部分
    iterator insert(iterator pos,const T& x)
    {
        if(_finish==_endofstorage)
        {
            size_t len=pos-_start;
            reserve(capacity()==0?4:capacity()*2);//扩容会引起迭代器失效
                   pos=_start+len;
       }
        

这是扩容的部分 代码 引起迭代器失效的罪魁祸首就是扩容操作里的

            T* tmp=new T[n];
            //拷贝旧空间数据到新空间
            if (_start)
            {
                  for(size_t i=0;i<old_size;i++)
                {
                    tmp[i]=_start[i];
                }
                    delete[] _start;
            }
            _start=tmp;

这里delete空间释放是导致其出现迭代器失效的罪魁祸首。

//erase部分
        iterator it=pos+1;
        while (it!=_finish)
        {
            *(it-1)=*it;
            it++;
        }
        _finish--;

而这里导致迭代器失效的主要原因就是因为 删除元素后 替他元素的前移 虽然指向的地址不变 但是指向的内容发生改变 所以迭代器失效哦。

解决方案

首先在std库中我们参考源代码
erase 提供了一个可以返回的 新的有效迭代器,指向 “被删除元素的下一个元素”,供用户更新迭代器,避免使用失效的迭代器。

假设我们有一个 vector 存储元素 [10, 20, 30, 40],内存布局如下(地址为示例):
请添加图片描述

  1. 删除 pos 指向的元素(30)
    执行 erase(pos) 时,vector 会将 pos 之后的元素(40)往前移动,覆盖 pos 位置的元素:
    40 从 0x100C 移到 0x1008(原 30 的位置)。

最终元素变为 [10, 20, 40],_finish 指向 0x100C(尾后位置)。

  1. 原 pos 迭代器指向的内容
    原 pos 迭代器本质是指针,仍指向 0x1008 这个内存地址,但该地址上的元素已从 30 变成了 40。
    此时:若解引用原 pos(*pos),会得到 40(而非被删除的 30)。

原 pos 迭代器在语义上已失效:它本应指向 “被删除的 30”,但现在指向的是 “原 30 后面的 40”,与预期逻辑不符。

  1. erase 返回值指向的内容
    erase 会返回 被删除元素的下一个元素的迭代器,即原 40 的位置(移动后 40 在 0x1008,下一个元素是尾后位置 0x100C)。

因此返回的迭代器指向 0x100C(新的尾后位置),是有效的。


insert的解决方案 则是如果扩容 通过记录和更新pos的方式来阻止迭代器失效

        if(_finish==_endofstorage)
        {
            size_t len=pos-_start;
            reserve(capacity()==0?4:capacity()*2);//扩容会引起迭代器失效
            //更新pos
            pos=_start+len;
            //指向新内存中插入的数值的位置,是有效的迭代器
       }

这里提一嘴 insert的返回值 始终指向新插入的元素的位置,是唯一有效的迭代器,需用它更新外部迭代器(比如你在 1 3 4 数组的 1后插入个2 那迭代器返回的就是2 的位置和元素)

7.3 作为函数参数时 能否写成&的形式

iterator insert(iterator& pos,const T& x) 能否这么写?
答案是 NO!

这里举个例子

    fcy::vector<int> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    v.push_back(4);
    v.insert(v.begin(), 0);

当我们想在开头位置传入0时候 如果以引用的方式写 会发生报错。
为什么?

    iterator begin()
    {
        return _start;
    }

当调用 insert(v.begin(), 0) 时,begin() 返回的是一个临时迭代器对象(右值 具有常性质)。C++ 规定 “非 const 左值引用(iterator&)不能绑定到右值”,此时编译会直接报错。
而这也就是我们常说的权限放大问题!
首先临时对象是具有常性的 具体为何这里会是临时对象 可以参考我以前写过的文章

7.4 reserve的两个注意点

大部分人一开始写这个功能都会这么写

   void reserve(size_t n)
    {
       if(n>capacity())
       {
           T* tmp=new T[n];
           //拷贝旧空间数据到新空间
           if (_start)
           {
               memcpy(tmp,_start,sizeof(T)*size());
               delete[] _start;
           }
           _start=tmp;
           _finish=_start+size();
           _endofstorage=_start+n;
       }
    }

乍一看没问题 其实漏洞百出

size()失效问题

    size_t size() const
    {
        return _finish-_start;
    }

问题就出现在了这三行代码中

_start=tmp;
_finish=_start+size();
_endofstorage=_start+n;

_finish = _start + size()中,size()的计算是_finish - _start,但此时_start已被更新为新空间的指针(tmp),旧的_finish指针已经失效,导致size()计算结果错误。

解决方案
记录原来的size()即可

size_t old_size=size();//提前记录原来的size
 _start=tmp;
 _finish=_start+old_size;
 _endofstorage=_start+n;

浅拷贝导致的内存管理问题

问题出现在这里

memcpy(tmp,_start,sizeof(T)*size());
delete[] _start;

memcpy是字节级别的浅拷贝,仅适用于内置类型(如int、double)。对于自定义类型(如std::string、包含指针成员的类),memcpy会直接拷贝指针值,导致新空间和旧空间的元素共用同一块内存,析构时会触发双重释放(delete[] _start释放旧空间后,新空间的元素指针仍指向已释放的内存,后续析构新空间时再次释放)。
解决方案
用循环赋值的方法

        if (_start)
        {
           // memcpy(tmp,_start,sizeof(T)*size());
            //string中的指针也会原样赋值 会导致str*指向同一空间
            //然后空间释放时候导致tmp所指向的也一样释放了
            //导致了浅拷贝
            //所以建议用赋值来解决
            for(size_t i=0;i<old_size;i++)
            {
                tmp[i]=_start[i];
            }
            //int这样的类型,赋值可以完成浅拷贝
            //string这样的类型,赋值重载完成深拷贝
            delete[] _start;
        }

7.5 vector 构造函数的模板实现及重载问题

当用 int 类型的 n 调用构造函数时,编译器可能无法区分是要匹配 vector(int n, T val),还是将 int 当作迭代器类型匹配 vector(it first, it last)(尤其是当 T 为整数类型时)。
在这之前我想先引入两个概念

贪婪匹配

“贪婪匹配”指的是模板函数 / 类会优先匹配能够满足参数类型的所有可能情况,即使存在非模板的重载版本,只要模板能匹配,就可能被优先选择。这种特性可能导致意外的匹配结果,尤其在重载函数与模板函数共存时容易出现问题。

template<class it>
vector(it first, it last) 
{
    while (first != last) {
        push_back(*first);
        first++;
    }
}

vector<int> v(5, 10); // 期望调用 vector(int n, T val)

此时,编译器可能会误将 5 和 10 视为 “迭代器类型 it = int”,尝试匹配模板构造函数,进而执行 *first(对 int 解引用),导致 “非法间接寻址” 错误 —— 这就是模板 “贪婪匹配” 引发的问题。

我曾尝试使用多种任意类型迭代器来进行初始化 也就是上面完整代码所展示的方法

    vector(int n,T val=T())
    {
        resize(n,val);
    }

    vector(size_t n,T val=T())
    {
        resize(n,val);
    }

当传入 int 时,会优先调用这个构造函数,避免被迭代器模板误匹配,从而解决 “非法间接寻址” 问题。
局限性:仍无法覆盖所有类型 并且fcy::vector<size_t> v6(10u,1u); 这么写依然报错 但是传vector<int> v6(10,1); 不报错
为什么 vector v(10, 1) 不报错?
因为写了一个 int 版本的构造函数 A:vector(int n, T val)(当 T=int 时,参数类型是 (int, int))。
此时
实参 10 和 1 是 int 类型,与 int 版本的构造函数 A 完全匹配。
构造函数 B 虽然也能推导 it=int(完全匹配),但 C++ 规定:“完全匹配的非模板函数,优先级高于完全匹配的模板函数”。(这里的 int 版本构造函数 A 是非模板,所以被优先选择,无歧义。)

为什么 size_t 版本会歧义?
参数类型是 (size_t, size_t)(因为 T=size_t),与实参类型完全匹配。
模板参数 it 会被推导为 size_t,参数类型也是 (size_t, size_t),与实参类型也完全匹配。
int 是 C++ 原生的 “基本类型”,而非模板的 int 版本构造函数 ,在编译器眼里是 “更专用” 的非模板函数,优先级明确高于模板。size_tunsigned int 的 “别名”(不是原生基本类型),编译器会觉得 size_t 版本二者的 “专用程度” 差不多。加上两者参数类型完全匹配,编译器无法判断哪个更 “合适”,最终报 “歧义错误”。

用类型萃取限制模板构造函数(目前仅了解)

必须通过类型萃取明确区分 “迭代器” 和 “整数类型”,让模板构造函数仅在参数是迭代器时生效。 具体操作就不写了,目前知识还暂时解决不了,仅做了解即可!

7.6 Tips

模版赋值

template<class T>
vector(int n,T val=T())

模版赋值可以采用匿名对象的方式进行赋值

缺省赋值

    iterator _start=nullptr;
    iterator _finish=nullptr;//最后一个数据的下一个
    iterator _endofstorage=nullptr;//已开辟内存的最后一个字节的下一个位置

缺省值是要走初始化列表的 这么写可以让初始化更方便

//    vector():
//    _start(nullptr)
//    ,_finish(nullptr)
//    ,_endofstorage(nullptr)
//    {}
    //因为有了缺省值所以还可以
   vector()
   {}
    
    //c++11强制编译器生成默认构造
    vector()=default;

多参数构造

才用初始化列表的方式

vector(initializer_list<T> il)
    {
        reserve(il.size());
        for(auto& e:il)
        {
            push_back(e);
        }
    }

创作不易 求三连!

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值