【C++修炼之路】10. vector类(二)

简介: 【C++修炼之路】10. vector类(二)

3.深拷贝问题


3.1vector>

微信图片_20230222011813.png

对于下面的代码,我们在上面模拟实现的所有成员函数的基础上观察:


void test_vector9()
{
    vector<vector<int>> vv;
    vector<int> v(5, 1);
    vv.push_back(v);
    vv.push_back(v);
    vv.push_back(v);
    vv.push_back(v);
    for (size_t i = 0; i < vv.size(); i++)
    {
        for (size_t j = 0; j < vv[i].size(); j++)
        {
            cout << vv[i][j] << " ";
        }
        cout << endl;
    }
    cout << endl;
}


微信图片_20230225121904.png


微信图片_20230225121907.png

结果不出我们所料,我们所模拟实现的vector也是支持T为vector类型的。


3.1.1提出问题


但如果我们再加上一个 vv.push_back(v);,看看会发生什么情况:


void test_vector9()
{
    vector<vector<int>> vv;
    vector<int> v(5, 1);
    vv.push_back(v);
    vv.push_back(v);
    vv.push_back(v);
    vv.push_back(v);
    vv.push_back(v);
    for (size_t i = 0; i < vv.size(); i++)
    {
        for (size_t j = 0; j < vv[i].size(); j++)
        {
            cout << vv[i][j] << " ";
        }
        cout << endl;
    }
    cout << endl;
}
}


微信图片_20230225122005.png

没错,就是你所想到的,扩容发生了问题!



3.1.2进行分析


那么为什么出现了这样的情况呢?根据我们的经验,不难猜想:大概是因为由于异地扩容之后,产生了浅拷贝,即我们异地扩容产生的变量的指向仍然是之前指向的位置,并且由于异地扩容之后,会delete[]原空间,这就导致异地扩容的指向也变成了野指针。

微信图片_20230222012034.png


当我们到了第五个push_back,也就是需要扩容的时候,我们发现:tmp与原本_start的位置指向的是同一个位置(注意外部的_start与内部的第一个_start指向的位置是一样的),这是由于memcpy引起的,而我们知道,memcpy所引起的异地扩容会释放旧空间,即释放旧位置所指向的位置,但这一释放,就导致了新开辟的tmp内部的指针变量指向的空间也被释放了

微信图片_20230222012037.png

因此,我们知道这是由于reserve中的memcpy所造成的的浅拷贝导致的,那么如何进行处理呢?


3.1.3 解决方式


既然浅拷贝的memcpy不行,那我们就可以通过赋值的方式在拷贝中开辟新空间,进行深拷贝:


微信图片_20230225122115.png


即将reserve中的memcpy换成如图所示的方式,这样赋值拷贝会开辟新空间(上面的代码中就是开辟了新空间),我们就可以避免浅拷贝的问题,那我们来看看结果:


微信图片_20230225122119.png


这样就解决了浅拷贝的问题了。


对于此类情况,事实上是很难发现的,并且处理的方式也不一定想到,因此我们一定要多多积累经验,才能在遇到困难的时候发现问题的关键所在。


因此我们同样也需要注意: 在C++中要避免使用C语言中的函数:memcpy、realloc、malloc等(realloc原地扩还好,若是异地扩容,就会发生我们所提到的错误)


3.2 vector< string >


事实上,stringvector<int>的道理是相同的,如果我们仍然用memcpy,会发现在需要扩容的过程中仍然出现浅拷贝造成的错误:

1. 用memcpy


void test_vector10()
{
    vector<string> v;
    v.push_back("1111111111111");
    v.push_back("1111111111111");
    v.push_back("1111111111111");
    v.push_back("1111111111111");
    for (size_t i = 0; i < v.size(); i++)
    {
        for (size_t j = 0; j < v[i].size(); j++)
        {
            cout << v[i][j] << " ";
        }
        cout << endl;
    }
    cout << endl;
}


微信图片_20230225122231.png


没有扩容,可以正常运行。


void test_vector10()
{
    vector<string> v;
    v.push_back("1111111111111");
    v.push_back("1111111111111");
    v.push_back("1111111111111");
    v.push_back("1111111111111");
    v.push_back("1111111111111");//扩容
    for (size_t i = 0; i < v.size(); i++)
    {
        for (size_t j = 0; j < v[i].size(); j++)
        {
            cout << v[i][j] << " ";
        }
        cout << endl;
    }
    cout << endl;
}

微信图片_20230225122327.png

引发异常:浅拷贝造成的。

2. 用赋值拷贝

即将memcpy变成赋值拷贝的形式。


微信图片_20230225122333.png

扩容也不会出错。


3.3深拷贝问题的总结


多加一个小标题string的目的就是方便我们去理解在自定义类型的情况下都会发生这种扩容出现的问题,而对于内置类型并不会发生,这次学过之后,我们也都应该对这种问题变得敏一些。


4.vector模拟实现的函数汇总



对于这个汇总,我将各个成员函数都集中起来,由于篇幅过长,具体的测试就没必要展示了,我会把完整的代码链接放在最后。

4.1 vector.h


#pragma once
namespace cfy
{
    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;
        }
        T& operator[](size_t pos)
        {
            assert(pos < size());
            return _start[pos];//注意
        }
        const T& operator[](size_t pos) const//重载
        {
            assert(pos < size());
            return _start[pos];
        }
        vector()//构造
            :_start(nullptr)
                ,_finish(nullptr)
                ,_endofstorage(nullptr)
            {}
        传统写法 v2(v1)
        //vector(const vector<T>& v)
        //  :_start(nullptr)
        //  , _finish(nullptr)
        //  , _endofstorage(nullptr)
        //{
        //  //_start = new T[v.capacity()];
        //  //……
        //  reserve(v.capacity());
        //  for (const auto& e : v)//必须&, 否则会拷贝构造调用拷贝构造,因为如果每一个元素(T)也都是vector,这就会导致拷贝构造调用拷贝构造
        //  {
        //    push_back(e);
        //  }
        //}
        vector(size_t n, const T& val = T())
            :_start(nullptr)
                , _finish(nullptr)
                , _endofstorage(nullptr)
            {
                reserve(n);
                for (size_t i = 0; i < n; i++)
                {
                    push_back(val);
                }
            }
        vector(int n, const T& val = T())//解决vector8的注释中的问题
            :_start(nullptr)
                , _finish(nullptr)
                , _endofstorage(nullptr)
            {
                reserve(n);
                for (size_t i = 0; i < n; i++)
                {
                    push_back(val);
                }
            }
        template <class InputIterator>
            vector(InputIterator first, InputIterator last)
            :_start(nullptr)
                , _finish(nullptr)
                , _endofstorage(nullptr)
            {
                while (first != last)
                {
                    push_back(*first);
                    ++first;
                }
            }
        //现代写法
        vector(const vector<T>& v)
            :_start(nullptr)
                , _finish(nullptr)
                , _endofstorage(nullptr)
            {
                vector<T> tmp(v.begin(), v.end());
                swap(tmp);//this和tmp进行swap
            }
        ~vector()//析构
        {
            delete[] _start;
            _start = _finish = _endofstorage = nullptr;
        }
        //v1 = v2
        //v1 = v1;虽然会付出代价,但是能保证不会出错,极少数情况,能保证正确性,所以可以容忍
        vector<T>& operator=(vector<T> v)//由于直接swap,因此不能传引用
        {
            swap(v);
            return *this;
        }
        void reserve(size_t n)//注意迭代器失效问题
        {
            if (n > capacity())
            {
                size_t oldSize = size();
                T* tmp = new T[n];
                if (_start)
                {
                    //memcpy(tmp, _start, sizeof(T) * size());
                    for (size_t i = 0; i < size(); i++)
                    {
                        tmp[i] = _start[i];
                    }
                    delete[] _start;
                }
                _start = tmp;
                _finish = tmp + oldSize;
                _endofstorage = _start + n;
            }
        }
        void resize(size_t n, T val = T())
        {
            if (n > capacity())
            {
                reserve(n);
            }
            if (n > size())
            {
                while (_finish < _start + n)
                {
                    *_finish = val;
                    ++_finish;
                }
            }
            else
            {
                _finish = _start + n;
            }
        }
        bool empty() const
        {
            return _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)
            {
                size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
                reserve(newCapacity);
            }
            *_finish = x;
            ++_finish;
        }
        void pop_back()
        {
            assert(!empty());
            --_finish;
        }
        //迭代器失效问题:野指针问题:异地扩容导致
        iterator insert(iterator pos, const T& val)//不传引用是因为有左值的影响:常量、v.begin()
        {
            assert(pos >= _start);
            assert(pos < _finish);
            if (_finish == _endofstorage)
            {
                size_t len = pos - _start; // 处理失效问题,记录相对位置
                size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
                reserve(newCapacity);
                //扩容导致pos迭代器失效,需要更新处理一下
                pos = _start + len;
            }
            //挪动数据
            iterator end = _finish - 1;
            while (end >= pos)
            {
                *(end + 1) = *end;
                end--;
            }
            *pos = val;
            ++_finish;
            return pos;
        }
        iterator erase(iterator pos)
        {
            assert(pos >= _start && pos < _finish);
            iterator begin = pos + 1;
            while (begin < _finish)
            {
                *(begin-1) = *begin;
                ++begin;
            }
            --_finish;
            return pos;
        }
        void swap(vector<T>& v)
        {
            std::swap(_start, v._start);
            std::swap(_finish, v._finish);
            std::swap(_endofstorage, v._endofstorage);
        }
        void clear()
        {
            _finish = _start;//不能置空,会发现内存泄漏
        }
        private://成员变量和_size _capacity的本质是一样的,只不过表示方法不一样
        iterator _start;
        iterator _finish;
        iterator _endofstorage;
    };
}

4.2test.cpp

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<vector>
#include<string>
#include<stdlib.h>
#include<assert.h>
using namespace std;
#include"vector.h"//注意包头文件的顺序,std要在vector.h的上面,因为预处理头文件会展开
                  //会存在std命名空间的函数,因此std需要在上面
int main()
{
  try
  {
    cfy::test_vector10();
  }
  catch (const exception& e)
  {
    cout << e.what() << endl;
  }
  return 0;
}


相关文章
|
27天前
|
存储 编译器 C语言
【c++丨STL】vector的使用
本文介绍了C++ STL中的`vector`容器,包括其基本概念、主要接口及其使用方法。`vector`是一种动态数组,能够根据需要自动调整大小,提供了丰富的操作接口,如增删查改等。文章详细解释了`vector`的构造函数、赋值运算符、容量接口、迭代器接口、元素访问接口以及一些常用的增删操作函数。最后,还展示了如何使用`vector`创建字符串数组,体现了`vector`在实际编程中的灵活性和实用性。
51 4
|
28天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
50 2
|
9天前
|
存储 对象存储 C++
C++ 中 std::array<int, array_size> 与 std::vector<int> 的深入对比
本文深入对比了 C++ 标准库中的 `std::array` 和 `std::vector`,从内存管理、性能、功能特性、使用场景等方面详细分析了两者的差异。`std::array` 适合固定大小的数据和高性能需求,而 `std::vector` 则提供了动态调整大小的灵活性,适用于数据量不确定或需要频繁操作的场景。选择合适的容器可以提高代码的效率和可靠性。
30 0
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
103 5
|
13天前
|
存储 编译器 C语言
【c++丨STL】vector模拟实现
本文深入探讨了 `vector` 的底层实现原理,并尝试模拟实现其结构及常用接口。首先介绍了 `vector` 的底层是动态顺序表,使用三个迭代器(指针)来维护数组,分别为 `start`、`finish` 和 `end_of_storage`。接着详细讲解了如何实现 `vector` 的各种构造函数、析构函数、容量接口、迭代器接口、插入和删除操作等。最后提供了完整的模拟实现代码,帮助读者更好地理解和掌握 `vector` 的实现细节。
25 0
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
88 4
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
107 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
32 4
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
32 4
|
2月前
|
存储 C++ 索引
【C++打怪之路Lv9】-- vector
【C++打怪之路Lv9】-- vector
26 1