【C++】vector问题解决(非法的间接寻址,迭代器失效 , memcpy拷贝问题)

简介: 不使用memcpy函数不就可以了,然后我们使用简单粗暴的赋值拷贝,这样就不会发生浅拷贝问题了!!!

送给大家一句话:

世界在旋转,我们跌跌撞撞前进,这就够了 —— 阿贝尔 加缪

vector问题解决

1 前言

我们之前实现了手搓vector,但是当时依然有些问题没有解决:

  1. 迭代器区间拷贝(非法的间接寻址问题)
  2. 迭代器失效问题
  3. 使用memcpy拷贝问题

接下来,我们一点一点来解决这些问题!!!

2 迭代器区间拷贝

来看这个这个构造函数

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

这个是对迭代器区间进行的构造函数,思路很简单,把迭代器区间的数据依次尾插就可以了(这里之所以另外使用一个新的模版,而不是使用vector类的模版,是为了兼容更多的数据类型)。这样就可以通过一个现有的类型来构造容器。

但是出乎意料的是出现了一个问题: C2100 非法的间接寻址 (编译层面的问题) 。非法的间接寻址的造成原因有很多:

  1. 空指针引I用:当一个指针没有被初始化或者为NULL时,对它进行间接寻址操作会导致非法访问
  2. 野指针引用:当一个指针超出了它所指向的内存范围,或者已经被释放但仍然被引用时,进行间接寻址操作也会导致非法访问。
  3. 类型不匹配:如果试图将指针转换为不兼容的类型进行间接寻址,也会导致非法访问。

我们分析一下我们遇到的问题是那种问题?空指针引用吗?不可能!野指针引用吗?也不可能!!! 那么真相只有一个:我们遇到了类型不匹配的问题,那这是来自哪里的呢???,经过我的排除法(注释不同的代码块来进行查找),得到了结果

vector<int> v1(5,6);

这一行代码是我们出错的根源,为什么这个构造没有去使用vector(size_t n,T val = T()),而是使用我们的vector(InputIterator first, InputIterator last),因为第二个函数与(5,6)的类型更匹配,编译器会寻找最合适的函数。

解决方法也是十分暴力:多枚举几个 构造函数:

vector(size_t n,T val = T())
vector(int n,T val = T());
vector(long long n,T val = T());

这样就会优先匹配vector(int n,T val = T());了,我们的问题也就解决了。

3 迭代器失效问题

这个问题主要出现在我们的插入操作(insert)和删除操作(erase)。来看:

void vector_test7() {
  vector<int> v1;
  v1.push_back(1);
  v1.push_back(2);
  v1.push_back(3);
  v1.push_back(4);
  v1.push_back(5);
  v1.push_back(6);
  v1.push_back(7);


  vector<int>::iterator it = v1.begin() + 3;// 4
  cout << *it << endl;
  v1.insert(3, 40);
  cout << *it << endl;
}

这个执行的结果是:

迭代器的指向发生了改变,我们实现的迭代器的底层是指针,我们插入之后指针位置不变,而数组元素改变,自然会产生不一样的结果。这个问题看起来不严重,那我们再来看:

void vector_test7() {
  vector<int> v1;
  v1.push_back(1);
  v1.push_back(2);
  v1.push_back(3);
  v1.push_back(4);
  v1.push_back(5);
  v1.push_back(6);
  v1.push_back(7);
  v1.push_back(8);

  vector<int>::iterator it = v1.begin() + 3;// 4
  cout << *it << endl;
  v1.insert(3, 40);
  cout << *it << endl;
}

为什么这里出现了乱码???我们代码和之前的区别是什么???一个进行了扩容,一个没进行扩容。扩容之后vector的_start发生了改变,自然我们的指针也失去了对应作用。 迭代器就失效了,这个解决办法也很简单,就是插入之后不要使用之前的迭代器!!!一定要对迭代器进行更新。

再来看erase中的问题:

void vector_test7() {
  vector<int> v1;
  v1.push_back(1);
  v1.push_back(2);
  v1.push_back(3);
  v1.push_back(4);
  v1.push_back(5);
  //v1.push_back(6);
  //v1.push_back(7);
  //v1.push_back(8);

  vector<int>::iterator it = v1.begin();
  //删除偶数
  while (it != v1.end())
  {
    if (*it % 2 == 0)
    {
      v1.erase(it);
    }
    ++it;
  }
  print_vector(v1);
}

这样运行起来是没有问题的,那么再来看:

void vector_test7() {
  vector<int> v1;
  v1.push_back(1);
  v1.push_back(2);
  v1.push_back(3);
  v1.push_back(4);
  v1.push_back(4);
  v1.push_back(5);
  //v1.push_back(6);
  //v1.push_back(7);
  //v1.push_back(8);

  vector<int>::iterator it = v1.begin();
  //删除偶数
  while (it != v1.end())
  {
    if (*it % 2 == 0)
    {
      v1.erase(it);
    }
    ++it;
  }
  print_vector(v1);
}

现在出现了:

这个问题,问题的来源也很简单,我们迭代器在删除之后没有改变位置,但是_start的元素发生了改变,也就是相当于 it 向后移动了两次,为了避免这个情况我们可以:

  while (it != v1.end())
  {
    if (*it % 2 == 0)
    {
      v1.erase(it);
    }
    else
    {
      ++it;
    }
    
  }

这样就可以了:

需要注意的一点是,我们的操作是以g++标准来进行的(如果删除会进行缩容,也会出现错误,迭代器就不能进行++了),所以 在VS环境下,vector 容器在erase 之后的迭代器是严格不能使用的,使用就会报错,因为VS迭代器的底层不是原生指针,判断有所不同。

迭代器失效解决方案总结

1. 删除插入之后更新对应迭代器!(erase删除后会返回新的迭代器 ,按规则进行迭代就可以了 it = v1.erase(it)

2. 插入删除之后不使用迭代器

4 memcpy拷贝问题

我们创建一个string类的容器,来看看能不能正常运行:

{void vector_test8() {
  vector<string> v1;
  v1.push_back("11111");
  v1.push_back("22222");
  v1.push_back("33333");
  v1.push_back("44444");
  v1.push_back("55555");

  print_vector(v1);
}

来看效果:

程序直接崩掉了,经过我们的调试,我们能打印出来正确的数据,但是走到程序最后的时候出现了错误,那么应该就是析构函数的问题了!

来画图分析一波:

  1. memcpy是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中
  2. 如果拷贝的是自定义类型的元素,memcpy既高效又不会出错,但如果拷贝的是自定义类型元素,并且自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅拷贝

结论:如果对象中涉及到资源管理时,千万不能使用memcpy进行对象之间的拷贝,因为memcpy是浅拷贝,否则可能会引起内存泄漏甚至程序崩溃

那么怎么解决呢???非常简单:

    //扩容
    void reserve(size_t newcapacity) {
      //记录位置
      size_t n = _finish - _start;
      T* tmp = new T[newcapacity];
      //拷贝
      //memcpy(tmp, _start, size() * sizeof(T));
      for (size_t i = 0; i < size(); i++)
      {
        tmp[i] = _start[i];
      }
      delete[] _start;
      _start = tmp;
      _finish = _start + n;
      _end = _start + newcapacity;
      
    }

不使用memcpy函数不就可以了,然后我们使用简单粗暴的赋值拷贝,这样就不会发生浅拷贝问题了!!!

相关文章
|
1月前
|
存储 C++ 索引
【C++打怪之路Lv9】-- vector
【C++打怪之路Lv9】-- vector
20 1
|
1月前
|
编译器 C++
【C++】—— vector模拟实现
【C++】—— vector模拟实现
|
1月前
|
算法 C++ 容器
C++之打造my vector篇(下)
C++之打造my vector篇(下)
26 0
|
1月前
|
存储 编译器 C++
C++之打造my vector篇(上)
C++之打造my vector篇(上)
25 0
|
1月前
|
算法 C++ 容器
【C++】—— vector使用
【C++】—— vector使用
|
1月前
|
存储 缓存 C++
C++番外篇——list与vector的比较
C++番外篇——list与vector的比较
21 0
|
1月前
|
C++
C++番外篇——vector的实现
C++番外篇——vector的实现
46 0
|
7天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
33 4
|
8天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
28 4
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4