背景:
我们手写一个顺序栈,展开接下来的实验:
⭐️ this指针指向的是类在内存中的起始位置class SeqStack { public: SqeStack(int size = 10) { cout << this << "SeqStack()" << endl; pstack_ = new int[size_]; top_ = -1; size_ = size; } ~SeqStack() { cout << this << " ~SeqStack()" << endl; delete[] pstack_; pstack_ = nullptr; } void push(int val) { if (full()) resize(); pstack_[++top_] = val; } void pop() { if (empty()) return; --top_; } int top() { return pstack_[top_]; } bool empty() { return top_ == -1; } bool full() { return top_ == size_ - 1; } private: int *pstack_; //动态开辟数组,存储顺序栈的元素 int top_; //指向栈顶元素的位置 int size_; //数组扩容的总大小 void resize () { int *ptem = new int[size_ * 2]; for (int i = 0; i < size_; i++) { ptmp[i] = pstack_[i]; } delete[] pstack_; pstack_ = ptmp; size_ *= 2 } };
文章目录
- 浅拷贝
- 自定义拷贝构造
- 为什么不用memcpy
- 赋值操作引起的浅拷贝问题
- 重载赋值运算符
浅拷贝
int main () {
SeqStack s1(10);
SeqStack s2 = s1; //#1
//SeqStack s3 = s1; #2 1和2都是调用拷贝构造
//SeqStack s4; s4 = s1 #这个才是调用operator=
return 0;
}
程序会直接崩溃,终端提示我们两次释放相同的内存空间,造成内存泄漏。
这是由于代码SeqStack s2 = s1;
明显是调用了默认的拷贝构造函数,默认的拷贝构造其实是一个浅拷贝。
可以看如下图:
所以说在析构的时候,先析构s2,导致我们new int[10]这块内存就已经没有了,s1中的pstack_成为了一个野指针,我们在析构一个已经释放了的内存。
总结:
对象默认的拷贝构造事做内存的数据拷贝。
关键是对象如果占用了外部资源,那么浅拷贝就出现问题了。
如果之后我们发现一个对象有一个指针,并且这个指针还指向一个外部的(堆)上的内存,所以我们一定要警防浅拷贝。此时我们一定不能依靠编译器为我们自动生成的拷贝构造
自定义拷贝构造
SeqStack(const SeqStack &src) {
//以下就是默认的浅拷贝操作
//pstack_ = src.pstack_;
//top_ = src.top_;
//size_ = src.size_;
pstack_ = new int[srtc.size_];
for (int i = 0; i <= src.top_; ++i) {
pstack_ = src.pstack_;
}
top_ = src.top_;
size_ = src.size_;
}
在这里我们就对指针类型做了一个深拷贝。
为什么不用memcpy
我们在进行数据拷贝的时候,都是用的for循环,而不用memcpy,这是为什么呢?
如图所示,我们需要把小内存的数据全部放到大内存上来实现扩容,或者数据的迁移。
因为我们在进行数据拷贝的时候,假如我们要把这块内存上的数据拷贝到那块内存上,如果这块内存上的数据仅仅是里面放int型,但是每一个整型都不占用该整型之外的资源(堆上的资源),就是说这块内存本身就只是放了一块值而已。
那使用内存的memcpy拷贝到那块大内存中,那是没有任何问题的。
那我们假设一下,这个数组里面放的不是整型,而是对象,而且每一个对象里面都有指针,而且还指向了外部的资源,也就是说这个数组里面存的对象的浅拷贝是有问题的。
比如说我们的ptmp[i]里面放的不是整型而是对象,那么我用memcpoy的话就只是把对象本身的内存拷贝了一份,这做的都是浅拷贝的操作。
浅拷贝的问题就是,让我们拷贝完的对象里面由于指针跟我们原来对象内存里面的指针指向的都是同一块资源,等我们拷贝完之后,删除小内存中的对象,会自动调用析构函数,析构函数会释放对象资源,那不就直接把我们打指针对象指向的那块堆内存也释放掉了。导致我们拷贝的那些对象的指针都成为了野指针。
所以说面向对象编程里面,数据的拷贝必须得用for循环来防止内存泄漏的问题。
赋值操作引起的浅拷贝问题
int main () {
SeqStack s1(10);
SeqStack s2 = s1; //#1
//SeqStack s3 = s1; #2 1和2都是调用拷贝构造
//SeqStack s4;
s4 = s3; //赋值操作=》做直接的潜拷贝
return 0;
}
}
同理,如果我们不在类里面不重载赋值运算符,编译器会为我们调用默认的赋值操作。
我们仍然会在第二次析构的时候出现问题。
所以我们需要重载赋值运算符operator=
重载赋值运算符
void operator= (const SeqStack &src) {
pstack_ = new int[srtc.size_];
for (int i = 0; i <= src.top_; ++i) {
pstack_ = src.pstack_;
}
top_ = src.top_;
size_ = src.size_;
}
至此,完美解决。