C++类与对象(四)
上期我们介绍了构造函数和析构函数,这期我们来介绍拷贝函数和运算符重载
拷贝函数
在现实生活中,可能存在另一个你。
那在C++中,我们是否能创建一个与已知对象一样的新对象呢?
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
特征
一、拷贝构造函数是构造函数的一个重载形式
二、拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归。
为什么参数必须用引用
在C语言中,Stack
栈这个数据结构实现,我们都是用Stack *s
等指针方式来传值传参,那么我们是否可以不通过指针,直接使用Stack s
来传值传参呢?
struct Stack
{
int *a;
int top;
int capacity;
}ST;
//初始化
void Init(Stack *s) {
s->capacity = 4;
s->a = (int*)malloc(sizeof(int) * s->capacity);
if (s->a == nullptr) {
perror("malloc fail!");
}
s->top = -1;
}
bool IsFull(Stack* s) {
return s->top == s->capacity - 1;
}
bool IsEmpty(Stack* s) {
return s->top == -1;
}
void Resize(Stack* s) {
s->capacity *= 2;
s->a = (int*)realloc(s->a, s->capacity * sizeof(int));
if (s->a == nullptr) {
perror("realloc fail!");
}
}
void Push(Stack* s,int x) {
if (IsFull(s)) {
Resize(s);
}
s->a[++s->top] = x;
}
void Pop(Stack* s) {
if (IsEmpty(s)) {
perror("Stack is Empty!");
exit(-1);
}
s->a[s->top--];
}
void Print(Stack *s) {
if (IsEmpty(s)) {
perror("Stack is Empty!");
exit(-1);
}
for (int i = 0; i <= s->top; i++) {
printf("%d ", s->a[i]);
}
printf("\n");
}
int main() {
Stack s;
Init(&s);
Push(&s, 1);
Push(&s, 2);
Push(&s, 3);
Push(&s, 4);
Push(&s, 5);
Push(&s, 6);
Push(&s, 7);
Push(&s, 8);
Print(&s);
return 0;
}
在以上代码中,是一个Stack
的数据结构,正常运行是没有问题的
当我们把Print
函数 Stack *s
换成Stacl s
时继续运行
bool IsEmpty(Stack s) {
return s.top == -1;
}
void Print(Stack s) {
if (IsEmpty(s)) {
perror("Stack is Empty!");
exit(-1);
}
for (int i = 0; i <= s.top; i++) {
printf("%d ", s.a[i]);
}
printf("\n");
}
相应的需要修改Pop
、IsEmpty
等函数(这里就不放出来,自行体会)
当然我们将此类传值传参方法放到类里面的拷贝构造函数是以下这样的
class Date {
public:
//构造函数
Date(int year = 1, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
//拷贝函数
Date(Date dd) {
_year = dd._year;
_month = dd._month;
_day = dd._day;
}//这里的参数是Date dd!!!
//Date类不需要析构函数,因为变量都是内置类型,由编译器回收资源
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1;//创建d1对象
Date d2(d1);//用拷贝函数拷贝生成d2对象,拷贝对象是d1
return 0;
}
注意拷贝构造函数的参数!
在VS2022中,会直接报错,而在老一些的编译器版本例如VS2013等可能不会对其优化,因此我们需要明白为什么C++的拷贝构造函数的参数为什么必须是引用,而不能直接使用类当参数。
C++的类中,实例化一个新对象,如果没有显示定义构造函数和拷贝构造函数,会自动生成默认的构造函数和拷贝构造函数
看以下的图,拷贝d1
时,不用引用传参会引起无穷递归,因为Date dd
也会调用他自身的拷贝函数,从而不断递归,引起无穷递归。
所以,参数必须是用引用
//拷贝函数
Date(Date& dd) {
_year = dd._year;
_month = dd._month;
_day = dd._day;
}//引用传参
浅拷贝和深拷贝
浅拷贝
根据上面的内容。我们成功构建了Date
类的拷贝构造函数,接下来我们调试以上修改好的程序
成功把d1
的内容拷贝到d2
那么是不是所有类都可以这样子拷贝呢?答案是:不可以!!!!!!!!!
用Queue
队列这个类演示一下
class Stack {
public:
//构造函数
Stack(int capacity=4) {
_array = (int*)malloc(sizeof(int) * capacity);
if (_array == nullptr) {
perror("malloc fail");
exit(-1);
}
_capacity = capacity;
_size = 0;
}
//拷贝函数
Stack(Stack& ST) {
_array = ST._array;
_capacity = ST._capacity;
_size = ST._size;
}
//析构函数
~Stack() {
if (_array != nullptr) {
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
int* _array;
int _capacity;
int _size;
};
class Queue {
Stack push;
Stack pop;
int _size = 0;
};
int main() {
Queue q1;
Queue q2(q1);
return 0;
}
编译一下是显示没有语法错误的,然鹅运行程序时却
为什么会出现错误呢?最主要的原因是拷贝函数只进行了浅拷贝也叫值拷贝,没有进行深度拷贝也叫深拷贝。
原因剖析:
创建好q1
对象后,继续执行拷贝构造q2
对象
已经执行完毕拷贝构造,当继续执行时会自动调用q1
的析构函数,也就是Stack push
和Stack pop
的析构函数~Stack
,此时q2
是浅拷贝,也就是q2
里面的push 和pop类
的地址与q1
是一样的,当出了q1
的作用域第一次调用析构函数,当出了q2
的作用域会第二次调用析构函数。从而导致了释放两次_array
的空间导致报错。
简单来说就是当 q1
和 q2
退出作用域时,q1
和 q2
中的 Stack
成员 push
和 pop
会调用各自的析构函数。由于它们共用了相同的 _array
,这会导致同一块内存被 free
两次,从而引发错误。
想要解决这个办法,就需要用到深拷贝
深拷贝
既然直接拷贝不行,我们可以新开辟一个内存空间,来拷贝q1
的所有内容,这种开辟新空间的拷贝操作叫深拷贝
//拷贝函数
Stack(Stack& ST) {
//浅拷贝
/*_array = ST._array;
_capacity = ST._capacity;
_size = ST._size;*/
//深拷贝
_array = (int*)malloc(sizeof(int) * ST._capacity);
if (_array == nullptr) {
perror("Copy fail!");
exit(-1);
}
//将ST的内用拷贝到新创建的_array数组
memcpy(_array, ST._array, sizeof(int) * ST._size);
_capacity = ST._capacity;
_size = ST._size;
}
-
拷贝构造函数不会修改原对象(q1):
- 在调用拷贝构造函数时,
q1
的数据只会被读取,不会被修改,也不会重新分配内存。
- 在调用拷贝构造函数时,
-
新对象(q2)有自己独立的内存:
q2
在拷贝构造函数中通过malloc
分配了一块新的内存,并将q1
的数据复制到这块内存中,因此q1
和q2
的内存是独立的。
-
原对象(q1)的内存地址不变:
- 在整个拷贝过程中,
q1
的_array
地址不会发生变化,因为拷贝构造函数只为新对象分配内存。
- 在整个拷贝过程中,
-
malloc
执行两次,但只针对不同对象:q1
在构造时执行了一次malloc
,q2
在拷贝构造时为自己执行了一次malloc
,这两次分配是独立的,互不影响。
因此,拷贝函数是一个特殊的函数,并不会改变原对象的内容,而仅仅是作拷贝用。调试程序,可以看见q1
和q2
的_array
的地址是不一样的,是两块独立的内存空间。而这个过程中q1
的_array
是不变的
总结
**浅拷贝:**浅拷贝只复制对象的基本属性和指针,而不复制指针所指向的实际数据。这意味着源对象和目标对象中的指针会指向同一块内存。
特点:快速、节省内存。
可能导致问题:当一个对象被销毁时,它的指针所指向的内存也会被释放,另一个对象也会因为释放导致无效。
适用情况:适合于没有动态内存分配或者不需要独立对象的情况。例如Date
类只有内置类型的类
深拷贝:深拷贝会复制对象及其所指向的所有数据,包括指针指向的内容。这意味着每个对象都有自己独立的内存。
特点:比浅拷贝更耗费时间和内存,因为需要为每个指针分配新的内存并复制数据。
避免了悬空指针的问题,因为每个对象都持有自己的数据副本。
适用情况:适合于含有动态内存分配的对象,或需要独立副本的情况。例如队列、栈、二叉树等需要开辟空间的数据结构。