【C++小白逆袭】vector深拷贝原来这么简单!从崩溃到精通的逆袭之路

288 阅读9分钟

【C++小白逆袭】vector深拷贝原来这么简单!从崩溃到精通的逆袭之路 🚀

哈喽,各位C++萌新小伙伴们!👋 最近是不是被STL容器搞得头都大了?尤其是vector的拷贝问题,明明看着代码没问题,一运行就崩溃,还不知道错在哪儿?😭 今天这篇文章就带你彻底搞懂vector的深拷贝,从浅拷贝的"坑"里爬出来,成为STL小能手!话不多说,咱们发车啦!🚗💨

😱 浅拷贝的"致命陷阱":共享钥匙的灾难

先问大家一个问题:如果你有一把房间钥匙🔑,复制了一把给朋友,结果你把房间拆了(释放内存),朋友拿着钥匙还能开门吗?答案肯定是不能!这就是浅拷贝的真实写照——只复制指针,不复制实际数据,导致多个对象共享同一块内存,最后"同归于尽"!

举个🌰:浅拷贝是如何搞崩程序的

咱们来看一段"作死"代码(千万别学!):

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

class MyString {
public:
    char* data;  // 指向堆内存的指针

    // 构造函数:分配内存并初始化
    MyString(const char* str) {
        data = new char[strlen(str) + 1];  // 申请内存
        strcpy(data, str);  // 复制字符串
        cout << "构造函数:分配了内存 @" << (void*)data << endl;
    }

    // 浅拷贝构造函数(编译器默认生成的就是这个!)
    MyString(const MyString& other) {
        data = other.data;  // 只复制指针,不复制数据!
        cout << "浅拷贝:共享内存 @" << (void*)data << endl;
    }

    // 析构函数:释放内存
    ~MyString() {
        if (data) {
            delete[] data;  // 释放内存
            cout << "析构函数:释放了内存 @" << (void*)data << endl;
        }
    }
};

int main() {
    MyString s1("Hello");  // 创建对象s1
    MyString s2 = s1;      // 浅拷贝s1给s2(危险!)

    // 程序结束时,s1和s2都会析构,导致同一块内存释放两次!
    return 0;
}

运行结果(崩溃预警!):

构造函数:分配了内存 @0x55f8d2a2c2a0
浅拷贝:共享内存 @0x55f8d2a2c2a0
析构函数:释放了内存 @0x55f8d2a2c2a0  // s2先释放
析构函数:释放了内存 @0x55f8d2a2c2a0  // s1再释放,程序崩溃!!!

看到没?😱 浅拷贝就像两个人共用一把钥匙,一个人把房间拆了,另一个人还拿着钥匙去开门,不崩溃才怪!这就是为什么vector一定要实现深拷贝——因为它存储的元素可能是像MyString这样需要管理内存的对象!

🧐 深拷贝是什么:复制整个"房间"的魔法

既然浅拷贝这么坑,那深拷贝就是来拯救世界的!深拷贝不只是复制指针,而是重新分配一块内存,把数据完整复制过去,就像你不仅复制了钥匙,还复制了一整个一模一样的房间和里面的所有东西🔑→🏠→🏠,两个对象从此各过各的,互不干扰!

vector的深拷贝是如何实现的?

vector的深拷贝主要靠两个"法宝":拷贝构造函数重载赋值运算符。咱们先看原文中的核心代码(带萌新注释版):

法宝1:深拷贝构造函数 🛠️
vector(const vector<T>& v) 
    : _start(nullptr), _finish(nullptr), _endOfStorage(nullptr) {  // 初始化指针为空
    reserve(v.capacity());  // 提前预定和原vector一样大的空间(避免频繁扩容)
    const_iterator cit = v.cbegin();  // 原vector的只读迭代器(像个"小手"指向元素)
    iterator it = _start;             // 新vector的迭代器(新"小手")
    
    while (cit != v.cend()) {  // 遍历原vector的每个元素
        *it++ = *cit++;        // 逐个复制元素(重点!会调用元素的拷贝构造)
    }
    _finish = it;  // 更新新vector的末尾指针
}

萌新解读
这就像搬家公司帮你搬家📦:先租一个和原来一样大的新公寓(reserve预定空间),然后派两个"搬运工"(cit和it),把旧公寓的东西一件件搬到新公寓(*it++ = *cit++),最后确定新家的边界(_finish = it)。

法宝2:重载赋值运算符 = 🔄
vector<T>& operator=(vector<T> v) {  // 参数v是拷贝构造的临时对象(深拷贝来的!)
    swap(v);  // 交换当前对象和临时对象v的指针(偷天换日!)
    return *this;  // 返回当前对象
}

// 自定义swap函数:只交换指针,不复制数据(超高效!)
void swap(vector<T>& v) {
    std::swap(_start, v._start);
    std::swap(_finish, v._finish);
    std::swap(_endOfStorage, v._endOfStorage);
}

萌新解读
这招叫"狸猫换太子"😏!别人给你一个vector(v),你先让它自己深拷贝一份(参数v是拷贝构造的结果),然后把自己的"旧房子钥匙"(指针)和v的"新房子钥匙"交换,最后让v拿着你的旧钥匙离开(函数结束时v自动析构,释放旧内存)。既安全又高效,简直不要太聪明!

🤔 嵌套vector的深拷贝:书包里的小书包怎么复制?

如果你以为到这里就结束了,那就太天真啦!当vector嵌套vector时(比如vector<vector<int>>),深拷贝会进入"套娃模式"🧸→🧸→🧸!咱们用"书包理论"来解释:

  • 外层vector是个大书包,里面装着小书包(内层vector)
  • 拷贝大书包时,不仅要复制大书包本身,还要把里面的每个小书包都复制一遍
  • 小书包里装着书(int),所以复制小书包时直接复制书就行

套娃过程演示:

vector<vector<int>> b = {{1,2}, {3,4}};  // 大书包b,里面有两个小书包
vector<vector<int>> a(b);  // 拷贝构造a(深拷贝开始!)

// 第一层:拷贝大书包a
a的_start = 新地址,调用vector<vector<int>>的拷贝构造函数
→ 遍历b的每个元素(小书包vector<int>)
→ 对每个小书包调用 *it++ = *cit++(触发第二层深拷贝)

// 第二层:拷贝小书包
每个小书包vector<int>调用自己的拷贝构造函数
→ 遍历小书包里的int元素,直接复制(int是内置类型,浅拷贝安全)
→ 小书包复制完成,大书包的元素也复制完成

// 最终:a和b完全独立,修改a的小书包不会影响b!

是不是像剥洋葱一样,一层一层复制?🧅 这就是深拷贝的强大之处——无论嵌套多少层,都能保证每个对象拥有独立的内存!

💥 浅拷贝的经典坑:memcpy为什么会搞崩程序?

很多小伙伴写vector时喜欢用memcpy拷贝数据,觉得"字节拷贝多高效啊!"😎 但这在自定义类型(比如String、vector)面前就是"自杀行为"!咱们看个例子:

反面教材:用memcpy实现reserve(会崩溃!)

void reserve(size_t n) {
    if (n > capacity()) {
        T* tmp = new T[n];  // 新空间
        memcpy(tmp, _start, sizeof(T)*size());  // 字节拷贝(浅拷贝!)
        delete[] _start;  // 释放旧空间(析构旧对象)
        _start = tmp;
        _finish = tmp + size();
        _endOfStorage = tmp + n;
    }
}

为什么会崩?
memcpy是"冷血搬运工"🧟‍♂️,只会按字节复制二进制数据,不会调用对象的拷贝构造函数!如果T是String(带指针成员):

  1. 旧空间的String对象被delete[]析构,释放了_str指针
  2. 新空间的String对象_str指针还是旧地址(被memcpy复制过来的),变成野指针
  3. 再次使用新空间的String时,访问野指针→程序崩溃!💥

正确做法:用赋值运算符逐个拷贝

void reserve(size_t n) {
    if (n > capacity()) {
        size_t old_size = size();
        T* tmp = new T[n];  // 新空间
        for (size_t i = 0; i < old_size; i++) {
            tmp[i] = _start[i];  // 调用T的赋值运算符(深拷贝!)
        }
        delete[] _start;  // 释放旧空间
        _start = tmp;
        _finish = tmp + old_size;
        _endOfStorage = tmp + n;
    }
}

区别tmp[i] = _start[i]会调用T的赋值运算符,比如String的operator=,确保每个元素都深拷贝,而不是简单的字节复制。这就是为什么STL源码里绝对不会用memcpy拷贝自定义类型!

🎯 实战演练:自己动手实现深拷贝vector

光说不练假把式!咱们来简化实现一个支持深拷贝的vector(核心部分),带你过把瘾!💻

简化版深拷贝vector代码

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

template <class T>
class MyVector {
public:
    // 迭代器(本质是指针)
    typedef T* iterator;
    typedef const T* const_iterator;

    // 默认构造
    MyVector() : _start(nullptr), _finish(nullptr), _endOfStorage(nullptr) {}

    // 拷贝构造(深拷贝!)
    MyVector(const MyVector<T>& v) {
        reserve(v.capacity());  // 预定空间
        const_iterator cit = v.cbegin();
        iterator it = _start;
        while (cit != v.cend()) {
            *it++ = *cit++;  // 逐个复制元素(调用T的拷贝构造)
        }
        _finish = it;
    }

    // 重载赋值运算符(深拷贝!)
    MyVector<T>& operator=(MyVector<T> v) {  // v是拷贝构造的临时对象
        swap(v);  // 交换指针
        return *this;
    }

    // 交换函数
    void swap(MyVector<T>& v) {
        std::swap(_start, v._start);
        std::swap(_finish, v._finish);
        std::swap(_endOfStorage, v._endOfStorage);
    }

    // 尾插元素
    void push_back(const T& val) {
        if (_finish == _endOfStorage) {  // 空间不足,扩容
            size_t newcap = capacity() == 0 ? 4 : capacity() * 2;
            reserve(newcap);
        }
        *_finish++ = val;  // 调用T的赋值运算符
    }

    // 扩容(关键!用赋值拷贝元素)
    void reserve(size_t n) {
        if (n > capacity()) {
            size_t old_size = size();
            T* tmp = new T[n];  // 新空间
            if (_start) {
                // 逐个拷贝元素(调用T的赋值运算符,深拷贝!)
                for (size_t i = 0; i < old_size; i++) {
                    tmp[i] = _start[i];
                }
                delete[] _start;  // 释放旧空间
            }
            _start = tmp;
            _finish = tmp + old_size;
            _endOfStorage = tmp + n;
        }
    }

    // 辅助函数
    size_t size() const { return _finish - _start; }
    size_t capacity() const { return _endOfStorage - _start; }
    iterator begin() { return _start; }
    iterator end() { return _finish; }
    const_iterator cbegin() const { return _start; }
    const_iterator cend() const { return _finish; }

    // 析构函数
    ~MyVector() {
        if (_start) {
            delete[] _start;
            _start = _finish = _endOfStorage = nullptr;
        }
    }

private:
    iterator _start;       // 数据起始地址
    iterator _finish;      // 数据末尾地址(下一个空位)
    iterator _endOfStorage;// 容量末尾地址
};

// 测试:用MyVector存储String(自定义类型)
class MyString {
public:  
    char* _str;
    MyString(const char* str = "") {
        _str = new char[strlen(str) + 1];
        strcpy(_str, str);
    }
    MyString(const MyString& s) {  // 深拷贝构造
        _str = new char[strlen(s._str) + 1];
        strcpy(_str, s._str);
    }
    MyString& operator=(const MyString& s) {  // 深拷贝赋值
        if (this != &s) {
            delete[] _str;
            _str = new char[strlen(s._str) + 1];
            strcpy(_str, s._str);
        }
        return *this;
    }
    ~MyString() { delete[] _str; }
};

int main() {
    MyVector<MyString> v;
    v.push_back(MyString("hello"));
    v.push_back(MyString("world"));

    MyVector<MyString> v2 = v;  // 深拷贝,不会崩溃!
    cout << "v2 size: " << v2.size() << endl;  // 输出2,完美!🎉

    return 0;
}

运行结果

v2 size: 2

没有崩溃!说明咱们的深拷贝实现成功啦!🥳 是不是很有成就感?

📝 总结:深拷贝通关秘籍

  1. 浅拷贝是坑:只复制指针,导致内存共享、重复释放(记住"共享钥匙"的比喻!)
  2. 深拷贝是救星:重新分配内存+复制数据,每个对象独立("复制整个房间")
  3. vector深拷贝靠两招
    • 拷贝构造函数:逐个复制元素(调用元素的拷贝构造)
    • 重载=运算符:拷贝+swap交换指针(高效又安全)
  4. 禁止用memcpy拷贝自定义类型:要用赋值运算符逐个拷贝(避免野指针)
  5. 嵌套vector会套娃拷贝:从外层到内层,每层都深拷贝(书包里的小书包)

到这里,你已经彻底搞懂vector的深拷贝啦!是不是发现其实没那么难?😎 记住,学习C++ STL的秘诀就是:多动手敲代码,多画内存图,遇到崩溃别害怕——每一次崩溃都是通往大神的阶梯!加油,未来的C++大佬就是你!🚀

微信图片_20250406182718.jpg

如果觉得这篇文章对你有帮助,别忘了点赞+收藏哦!有任何问题欢迎在评论区留言,我会一一回复哒~ 咱们下期再见!👋