【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(带指针成员):
- 旧空间的String对象被
delete[]析构,释放了_str指针 - 新空间的String对象
_str指针还是旧地址(被memcpy复制过来的),变成野指针! - 再次使用新空间的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
没有崩溃!说明咱们的深拷贝实现成功啦!🥳 是不是很有成就感?
📝 总结:深拷贝通关秘籍
- 浅拷贝是坑:只复制指针,导致内存共享、重复释放(记住"共享钥匙"的比喻!)
- 深拷贝是救星:重新分配内存+复制数据,每个对象独立("复制整个房间")
- vector深拷贝靠两招:
- 拷贝构造函数:逐个复制元素(调用元素的拷贝构造)
- 重载=运算符:拷贝+swap交换指针(高效又安全)
- 禁止用memcpy拷贝自定义类型:要用赋值运算符逐个拷贝(避免野指针)
- 嵌套vector会套娃拷贝:从外层到内层,每层都深拷贝(书包里的小书包)
到这里,你已经彻底搞懂vector的深拷贝啦!是不是发现其实没那么难?😎 记住,学习C++ STL的秘诀就是:多动手敲代码,多画内存图,遇到崩溃别害怕——每一次崩溃都是通往大神的阶梯!加油,未来的C++大佬就是你!🚀
如果觉得这篇文章对你有帮助,别忘了点赞+收藏哦!有任何问题欢迎在评论区留言,我会一一回复哒~ 咱们下期再见!👋