C++:vector容器(memcpy浅拷贝问题、迭代器失效问题)

news2024/10/2 10:43:41

文章目录

  • 一. `vector` 的介绍
  • 二. `vector` 的使用
    • 1. `string` 和 `vector<char>` 的区别
    • 2. 为什么 `vector` 没有 `find()` 接口
  • 三. `vector` 的模拟实现
    • 1. `vector` 的基本框架
    • 2. `memcpy` 和 `memmove` 的浅拷贝问题
    • 3. `vector` 迭代器失效问题
    • 4. 模拟代码

一. vector 的介绍

vector 的文档介绍

  1. vector是表示可变大小数组的序列容器
    vector 与C数组的共同点是两者底层数据都是顺序存放的,不同点vector 的大小可以动态改变,而数组一旦创建初始化后是不可以改变大小的。
  2. 既然 vector 的底层是连续存储的,那么也可以通过下标进行访问,随机访问的时间复杂度是 O ( 1 ) O(1) O(1)
  3. vector 分配空间策略:vector 会分配一些额外的空间以适应可能的增长,因此存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。
  4. vector 就是一个可以更改 _size 的数组,和数据结构阶段学习的顺序表是类似的。

二. vector 的使用

相关接口,参考文档。
vector 的接口相比 string 的要简洁许多,抛去了很多冗杂的接口。

1. stringvector<char> 的区别

既然 vector 可以存放 char 类型的元素,那么为什么还会有 string 这个类呢?
就比如下面的两个不同类的对象,它们的区别又是什么呢?

string s = "helloworld";  
vector<char> v(s.begin(), s.end());

(1). 两者底层所存放的数据是不一样的
在这里插入图片描述

string 类对象需要多开辟一块空间用来存放'\0',而 vector<char> 则是不需要的,虽然两者的 size() 都是 10,但是 string 类对象为了存放字符串末尾的 '\0', capacity() 已经扩容至了 15.


(2). string 特有的接口,vector 却不会提供

  • string 类重载了 += 以便两个字符串进行连接,vector 类则没有重载,vector<int> 使用 += 没有任何意义
  • 作为字符串,string 还提供了很多字符串特有的接口,例如:substr, c_str。而 vector 更重要是顺序存储、动态扩容的容器。

2. 为什么 vector 没有 find() 接口

vector 的顺序存储结构的查找效率是 O ( N ) O(N) O(N), 这个查找效率不是很高。
实际上,STL 没有提供 vectorfind() 接口,相比 vector, map, set 更适合用作查找的容器,mapset 的元素会按照红黑树进行存放,红黑树的查找效率是 O ( l o g N ) O(logN) O(logN)
各容器有各容器的优点,在使用容器的时候,需要根据自己的需求进行选择。

如果真的想对 vector 类对象进行查找,C++ STL提供了 <algorithm> 库,需要用时需要:

#include <algorithm>

在库中,有一个通用的 find() 模板方法:在指定迭代器区间 ( f i r s t , l a s t ] (first, last] (first,last]内查找某一个值 val
如果找到,返回指向 val 的迭代器;如果没找到,返回 last

在这里插入图片描述

int a[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
vector<int> v(a, a + 10); // 用 int数组 构造 vector<int>对象

vector<int>::iterator ret = find(v.begin(), v.end(), 11); // 使用通用find()方法进行查找

if (ret != v.end())
{
  cout << "找到了" << endl;
}
else
{
  cout << "没找到" << endl;
}

程序结果如下:在这里插入图片描述

三. vector 的模拟实现

模拟实现是为了更好地使用相关接口,而不是真的事无巨细实现成和源码一样。(模拟实现参考 STL3.0源码)

1. vector 的基本框架

先观察源码框架,主要是从成员变量、构造函数、插入逻辑来看。

在这里插入图片描述

由此可以大致清楚,vector 是用三个迭代器进行维护的,迭代器的底层则是指针。
在这里插入图片描述

相比使用_size_a, _capacity 的形式,使用迭代器则更加直接、方便。这样模拟实现 vector 的框架就搭好了。

源码的模板参数还有一个Alloc, 这是STL六大组件中的空间配置,使用内存池来分配对象空间,再用定位new进行初始化空间,可以减少new的消耗。
在模拟实现可以直接使用new进行开辟空间并初始化。

template <typename T>
class vector
{
  public:
    // 重命名迭代器,vector迭代器是原生指针
    typedef T *iterator;  
    typedef const T *const_iterator;

  private:
    iterator _start = nullptr;  // 指向数据块的开始
    iterator _finish = nullptr; // 指向有效数据的尾
    iterator _endofstorage = nullptr; // 指向存储容量的尾
}

相关接口如下:
在这里插入图片描述

大多数接口逻辑和 string 的是一致的,这里就不过多赘述,具体细节在文章结尾会展示出来。
下面主要谈谈在模拟实现中碰到的相关问题。

2. memcpymemmove 的浅拷贝问题

这个问题是在实现 reserve 出现的。

void reserve(size_t n)
{
  // 只有 n > 当前capacity,才真正会发生扩容
  if (n > capacity())
  {
    size_t old_size = size();
    iterator tmp = new T[n]; 
    if(_start) {
      memcpy(tmp, _start, sizeof(T) * old_size);
      delete[](_start);
    }
    
    _start = tmp;
    _finish = _start + old_size;
    _endofstorage = _start + n;
  }
}

上述代码,在 vector 元素是内置类型或者浅拷贝自定义类型不会出现问题,但是元素是深拷贝内置类型就会出现问题了。
例如, 下面的代码最终会在销毁 vstr 时发生崩溃:

vector<int> vi;
vi.push_back(1);
vi.push_back(1);
vi.push_back(1);
vi.push_back(1);
vi.push_back(1);

vector<string> vstr;
vstr.push_back("1111");
vstr.push_back("2222");
vstr.push_back("3333");
vstr.push_back("4444");
vstr.push_back("5555");

在这里插入图片描述

memcpy 是内存的二进制格式拷贝,在这里,直接将原来三个迭代器指向的地址原封不动的进行拷贝。

在这里插入图片描述

扩容后,此时被拷贝的 _str 指向了一块已经释放的空间,同时新开辟的空间也丢失,造成了内存泄漏。
之后,无论是要访问 _str 指向的空间,还是要释放 _str 指向的空间,程序都会运行崩溃,这是 memcpy 仅仅浅拷贝造成的。
虽然,内置类型浅拷贝自定义类型使用 memcpy 不会出现问题,为了容器的泛用性,需要进行修改。

解决方案: new 后,此时这块空间已经开辟并初始化了,可以直接拷贝构造并赋值。

  • 如果 T 是内置类型,会直接生成默认值
  • 如果 T 是自定义类型,会调用深拷贝,不会出现内存泄漏问题。
void reserve(size_t n)
{
  // 只有 n > 当前capacity,才真正会发生扩容
  if (n > capacity())
  {
    size_t old_size = size();
    iterator tmp = new T[n]; 

    if(_start) {
      // memcpy(tmp, _start, sizeof(T) * old_size);
      // memcpy 只是浅拷贝,由于已经申请并且初始化该空间,需要另外进行赋值
      for (size_t i = 0; i < old_size; ++i)
      {
        tmp[i] = _start[i];
      }
      delete[] (_start);
    }

    _start = tmp;
    _finish = _start + old_size;
    _endofstorage = _start + n;
  }
}

这样程序就不会因为 memcpy 的浅拷贝问题而崩溃了。
在这里插入图片描述

3. vector 迭代器失效问题

迭代器的主要作用是让算法能够不用关心底层数据结构,底层实际上就是一个指针,或者对指针进行了封装。

vector 的迭代器就是指针 T*, 因此,迭代器失效,实际上就是迭代器底层对应指针所指向的空间不是原本应该指向的空间,继续使用失效的迭代器,可能会造成程序崩溃

对于 vector 可能会导致其迭代器失效的操作有:

  • (1). 会引起底层空间改变的操作,都有可能导致迭代器失效,例如:resize,reserve,insert,push_back
    这些可能会扩容的操作,都有可能造成迭代器失效,扩容后,原来的迭代器仍然指向旧空间,而旧空间已经被释放归还操作系统了,这个时候仍然访问会造成程序崩溃。
vector<int> v{1, 2, 3, 4, 5, 6};
auto it = v.begin();  // it指向旧空间
v.reserve(100); // 扩容后旧空间被释放,it并没有指向新空间
while (it != v.end())
{
  cout << *it << " ";
  ++it;
}
cout << endl;

在 Linux gcc/g++ 下,程序运行如下:
gcc下,并不会直接停掉程序,没有相应的检查策略。这个程序中,恰好新的空间离原来的旧空间并没有太远,最终会得到下面的结果。
在这里插入图片描述

如果在打印前,进行迭代器的更新,就不会出现错误

vector<int> v{1, 2, 3, 4, 5, 6};
auto it = v.begin();
v.reserve(100);
it = v.begin(); // 更新
while (it != v.end())
{
  cout << *it << " ";
  ++it;
}
cout << endl;

程序运行如下: 在这里插入图片描述

  • (2). 指定位置的erase操作
    如果想删除 vector 中的所有偶数,思路肯定是用迭代器遍历,如果是偶数则删除
int a[] = {1, 2, 2, 4, 5, 6, 6, 8, 9};
vector<int> v(a, a + sizeof(a) / sizeof(int));

vector<int>::iterator it = v.begin();
while (it != v.end())
{
  if (*it % 2 == 0)
    v.erase(it);
  ++it;
}

for (auto e: v)
{
  cout << e << " ";
}
cout << endl;

结果却是不对的:在这里插入图片描述

问题关键就是在 erase 操作后,此时 it 已经指向了下一个元素,但是仍然执行 ++it, 导致跳过了一个元素。
在这里插入图片描述

这就是为什么 insert, erase 会返回一个迭代器了,返回的是原来的 pos 位置的迭代器。

正确程序应该如下:如果有删除操作,重新对 it 进行赋值,以保证不会跳过元素

int a[] = {1, 2, 2, 4, 5, 6, 6, 8, 9};
vector<int> v(a, a + sizeof(a) / sizeof(int));

vector<int>::iterator it = v.begin();
while (it != v.end())
{
  if (*it % 2 == 0)
  {
    it = v.erase(it);
  }
  else
  {
    ++it;
  }
}

for (auto e: v)
{
  cout << e << " ";
}
cout << endl;

程序最终结果正确:在这里插入图片描述

解决迭代器失效的策略:进行操作后重新对迭代器进行赋值

4. 模拟代码

#ifndef __VECTOR_H__
#define __VECTOR_H__

#include <cstring>
#include <assert.h>
#include <utility>

namespace wr
{
  template <typename T>
  class vector
  {
  public:
    // 重命名迭代器,vector迭代器是原生指针
    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; }

    //construct and destroy//
    vector () {}
    vector(int n, const T &value = T())
    {
      assert(n >= 0);

      reserve(n);
      while (_finish != _start + n)
      {
        push_back(value);
      }
    }
    template<typename InputIterator>
    vector(InputIterator first, InputIterator last)
    {
      assert(last >= first);

      reserve(last - first);
      while (first < last)
      {
        push_back(*first);
        ++first;
      }
    }
    vector(const vector<T>& v)
    {
      // reserve(v.capacity());
      // memcpy(_start, v._start, sizeof(T) * v.size());
      // _finish = _start + v.size();

      reserve(v.capacity());
      for (const auto &e :v)
      {
        push_back(e);
      }
    }
    vector<T>& operator=(vector<T> v)
    {
      if (this != &v)
        swap(v);

      return *this;
    }
    ~vector()
    {
      delete[] (_start);
      _start = _finish = _endofstorage = nullptr;
    }
    
    //capacity//
    size_t size() const { return _finish - _start; }
    size_t capacity() const { return _endofstorage - _start; }
    void reserve(size_t n)
    {
      // 只有 n > 当前capacity,才真正会发生扩容
      if (n > capacity())
      {
        size_t old_size = size();
        iterator tmp = new T[n]; 
        // memcpy(tmp, _start, sizeof(T) * old_size);
        // memcpy 只是浅拷贝,由于已经申请并且初始化该空间,需要另外进行赋值
        if(_start) {
          for (size_t i = 0; i < old_size; ++i)
          {
            tmp[i] = _start[i];
          }
          delete[] (_start);
        }

        _start = tmp;
        _finish = _start + old_size;
        _endofstorage = _start + n;
      }
    }
    void resize(size_t n, const T & value = T())
    {
      assert(n >= 0);

      reserve(n); // 先申请空间,避免下面push_back频繁new消耗内存
      if (n < size()) // 直接改变_finish
      {
        _finish = _start + n;
      }
      else if (n >size())  // 直接尾插
      {
        while (_finish != _start+n)
        {
          push_back(value);
        }
      }
    }

    //access//
    T& operator[](size_t pos) 
    {
      assert(pos >= 0);
      assert(pos < size());

      return *(_start + pos);
    }
    const T &operator[](size_t pos) const
    {
      assert(pos >= 0);
      assert(pos < size());

      return *(_start + pos);
    }
    
    //modify//
    void push_back(const T &x)
    {
      // 如果容量不够,先扩容
      if (_finish == _endofstorage)
      {
        reserve(capacity() == 0 ? 4 : 2 * capacity());
      }
      *_finish = x;
      ++_finish;
    }
    void pop_back()
    {
      assert(_finish > _start);

      --_finish;
    }
    iterator insert(iterator pos, const T& x)
    {
      // pos 之前插入数据
      assert(pos >= _start);
      assert(pos <= _finish);

      // 先记录pos相对_start的位置,扩容会更改pos指向元素的位置
      size_t old_pos = pos - _start;
      // 容量不够先扩容
      if (_finish == _endofstorage)
      {
        reserve(capacity() == 0 ? 4 : capacity() * 2);
      }
      
      // 更新pos,移动数据后插入数据
      pos = _start + old_pos;
      // memmove(pos + 1, pos, sizeof(T) * (_finish - pos));
      iterator end = _finish;
      while (end >= pos)
      {
        *(end + 1) = *end;
        --end;
      }
      ++_finish;
      *pos = x;

      return pos;
    }
    iterator erase(iterator pos)
    {
      assert(pos >= _start);
      assert(pos < _finish);

      //memmove(pos, pos + 1, sizeof(T) * (_finish - pos - 1));
      iterator it = pos + 1;
      while (it < _finish)
      {
        *(it - 1) = *it;
        ++it;
      }
      --_finish;

      return pos;
    }
    void swap(vector<T>& v)
    {
      std::swap(_start, v._start);
      std::swap(_finish, v._finish);
      std::swap(_endofstorage, v._endofstorage);
    }

  private:
    iterator _start = nullptr;  // 指向数据块的开始
    iterator _finish = nullptr; // 指向有效数据的尾
    iterator _endofstorage = nullptr; // 指向存储容量的尾
  };
} // namespace wr

#endif // vector.h

本章完。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1412913.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

mysql调优-Join多种连接方式

简单嵌套循环连接 r为驱动表&#xff0c;s为匹配表&#xff0c;可以看到从r中分别取出每一个记录去匹配s表的列&#xff0c;然 后再合并数据&#xff0c;对s表进行r表的行数次访问&#xff0c;对数据库的开销比较大 索引嵌套循环连接 这个要求非驱动表&#xff08;匹配表s&…

【Python】01快速上手爬虫案例一:搞定豆瓣读书

文章目录 前言一、VSCodePython环境搭建二、爬虫案例一1、爬取第一页数据2、爬取所有页数据3、格式化html数据4、导出excel文件 前言 实战是最好的老师&#xff0c;直接案例操作&#xff0c;快速上手。 案例一&#xff0c;爬取数据&#xff0c;最终效果图&#xff1a; 一、VS…

EMQX 单机及集群搭建

目录 1. 通过 Yum 源安装&#xff08;CentOS7 单机安装&#xff09; 1.1. 通过以下命令配置 EMQX Yum 源&#xff1a; 1.2. 运行以下命令安装 EMQX&#xff1a; 1.3. 运行以下命令启动 EMQX&#xff1a; 1.4. 访问 http://192.168.88.130:18083&#xff0c;默认用户名: adm…

【斯坦福计网CS144项目】Lab2 实现一个简单的 TCP 接收类

&#x1f57a;作者&#xff1a; 主页 我的专栏C语言从0到1探秘C数据结构从0到1探秘Linux &#x1f618;欢迎关注&#xff1a;&#x1f44d;点赞&#x1f64c;收藏✍️留言 &#x1f3c7;码字不易&#xff0c;你的&#x1f44d;点赞&#x1f64c;收藏❤️关注对我真的很重要&…

数据结构(C语言版)代码实现(四)——静态单链表的部分代码实现

目录 参考材料、格式 头文件SLinkList.h 库、宏定义、函数类型声明 线性表的静态单链表存储结构 按值查找 初始化静态链表 分配空间 回收空间 打印已用链表中的元素 求集合(A-B)U(B-A)中的元素&#xff08;重点介绍&#xff09; 调试过程 修改报错与警告 调试 完整…

何恺明 ResNet 引用量正式破20万!!!

注: 本文转自微信公众号 BravoAI (专注AI资讯和技术分享), 原文网址: 何恺明 ResNet 引用量正式破20万!!!, 扫码关注公众号 谷歌学术显示, 截止到 2024年1月26日, 何凯明 ResNet 一文引用量正式突破 20W!!! 更为惊人的是, 从论文发表到今天, 不过7年!!!‍‍‍‍‍‍‍‍‍‍‍‍…

6轴机器人运动正解-逆解控制【1】——三种控制位姿的方式

概览&#xff1a; 通过运动学正解控制机器人运动通过运动学逆解控制机器人运动一个简单的物体搬运&#xff08;沿轨迹运动&#xff09; 后续会陆续更新&#xff08;本例仅供学习交流用&#xff09; 一、6轴机器人 二、运动正解控制 通过修改各个轴的角度&#xff0c;实现末…

演示黄金票据,使用普通账户导入黄金票据创建域管理员

前提域搭建好了&#xff0c;域名是lin.com 首先我进入的是本机的用户不是域用户 我要是用本地用户&#xff0c;本地用户拿的票告诉我们这个TGS服务说我是域管账户administrator&#xff08;需要拿到域用户的哈希&#xff09; 此时进入到预控主机中&#xff08;人家是正儿八经…

Google Chrome RCE漏洞 CVE-2020-6507 和 CVE-2024-0517 流程分析

本文深入研究了两个在 Google Chrome 的 V8 JavaScript 引擎中发现的漏洞&#xff0c;分别是 CVE-2020-6507 和 CVE-2024-0517。这两个漏洞都涉及 V8 引擎的堆损坏问题&#xff0c;允许远程代码执行。通过EXP HTML部分的内存操作、垃圾回收等流程方式实施利用攻击。 CVE-2020-…

计算机网络 第6章(应用层)

系列文章目录 计算机网络 第1章&#xff08;概述&#xff09; 计算机网络 第2章&#xff08;物理层&#xff09; 计算机网络 第3章&#xff08;数据链路层&#xff09; 计算机网络 第4章&#xff08;网络层&#xff09; 计算机网络 第5章&#xff08;运输层&#xff09; 计算机…

MySQL十部曲之一:MySQL概述及手册说明

文章目录 数据库、数据库管理系统以及SQL之间的关系关系型数据库与非关系型数据库手册语法约定 数据库、数据库管理系统以及SQL之间的关系 名称说明数据库&#xff08;Database&#xff09;即存储数据的仓库&#xff0c;其本质是一个文件系统。它保存了一系列有组织的数据。数…

【第四天】蓝桥杯备战

题 1、求和2、天数3、最大缝隙 1、求和 https://www.lanqiao.cn/problems/1442/learning/ 解法&#xff1a;字符串方法的应用 import java.util.Scanner; // 1:无需package // 2: 类名必须Main, 不可修改public class Main {public static void main(String[] args) {Scann…

xshell无法连接linux,查询本机ip时出现<NO-CARRIER,BROADCAST,MULTICAST,UP>

在用xshell连接虚拟机VMware中的linux时&#xff0c;发现昨天还能连通的&#xff0c;今天连接不了了 我寻思应该是网卡配置出问题了&#xff0c;就去终端ip addr试了一下&#xff0c;果然发现问题&#xff0c;ip 查看网卡ens33就发现出现ens33:<NO-CARRIER,BROADCAST,MULTI…

操作符详解(上)

目录 操作符的分类 二进制和进制转换 2进制转10进制 10进制转2进制数字 2进制转8进制 2进制转16进制 原码、反码、补码 移位操作符 左移操作符 右移操作符 位操作符&#xff1a;&、|、^、~ 单目操作符 逗号表达式 操作符的分类 • 算术操作符&#xff1a; …

无法获得dpkg前端锁、Linux之E: 无法锁定管理目录(/var/lib/dpkg/),是否有其他进程正占用它?(解决方法)

无法获得dpkg前端锁的解决方法 sudo rm /var/lib/dpkg/lock sudo rm /var/lib/dpkg/lock-frontend sudo rm /var/cache/apt/archives/lock 输入以上三个命令即可解除占用。解除后&#xff0c;继续运行apt命令&#xff0c;已经顺利运行了。解除前端锁后&#xff0c;Linux之E: 无…

自动写作软件有哪些?一次性分享5个好用软件

自动写作软件有哪些&#xff1f;随着科技的不断发展&#xff0c;自动写作软件逐渐进入了人们的视野。这些软件能够根据用户提供的关键词和主题&#xff0c;自动生成一篇完整的文章。这对于需要大量内容创作者来说&#xff0c;无疑是一个福音。下面介绍一些知名的自动写作软件&a…

鸿蒙开发实战-手写文心一言AI对话APP

运行环境 &#xff08;后面附有API9版本&#xff0c;可修改后在HarmonyOS4设备上运行&#xff09; DAYU200:4.0.10.16 SDK&#xff1a;4.0.10.15 IDE&#xff1a;4.0.600 在DAYU200:4.0.10.16上运行 一、创建应用 1.点击File->new File->Create Progect 2.选择模版…

D. Gargari and Permutations

很好玩的一道类似LCS的DP 问题 定义dp(i) 为考虑最后一个字符串&#xff0c;且选择a&#xff08;i&#xff09;得到的最大LIS值 然后枚举所有小于i的位置&#xff0c;可以更新的条件是 所有的字符串中都有a[j]<a[i] 这个用map一处理就好了 #include<bits/stdc.h> usi…

EasyCVR视频融合平台雪亮工程视频智能监控方案设计与应用

随着科技的不断发展&#xff0c;视频监控已经成为城市安全防范的重要手段之一。为了提高城市安全防范水平&#xff0c;各地纷纷开展“雪亮工程”&#xff0c;即利用视频智能监控技术&#xff0c;实现对城市各个角落的全方位、全天候监控。本文将介绍一种雪亮工程视频智能监控方…

机器人学论文——智能施药机器人调研报告

目录 摘 要 Abstract 第一章&#xff1a;引言 1.1研究背景 1.2 研究意义 1.3文章架构 第二章&#xff1a;智能施药机器人发展现状 2.1引言 2.2 大田智能施药机器人发展现状 2.3 果园智能施药机器人发展现状 2.4 设施农业智能施药机器人发展现状 第三章&#xff1a;智能施药机器…