C++类与对象四

C++类与对象(四)

上期我们介绍了构造函数和析构函数,这期我们来介绍拷贝函数运算符重载

拷贝函数

在现实生活中,可能存在另一个你。

cv

那在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的数据结构,正常运行是没有问题的

image-20241020230529271

当我们把Print函数 Stack *s换成Stacl s时继续运行

image-20241020230921479

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");
}

相应的需要修改PopIsEmpty等函数(这里就不放出来,自行体会)

当然我们将此类传值传参方法放到类里面的拷贝构造函数是以下这样的

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;
}

注意拷贝构造函数的参数!

image-20241020231511530

在VS2022中,会直接报错,而在老一些的编译器版本例如VS2013等可能不会对其优化,因此我们需要明白为什么C++的拷贝构造函数的参数为什么必须是引用,而不能直接使用类当参数。

C++的类中,实例化一个新对象,如果没有显示定义构造函数和拷贝构造函数,会自动生成默认的构造函数和拷贝构造函数

看以下的图,拷贝d1时,不用引用传参会引起无穷递归,因为Date dd也会调用他自身的拷贝函数,从而不断递归,引起无穷递归。

image-20241021101614048

所以,参数必须是用引用

//拷贝函数
Date(Date& dd) {
	_year = dd._year;
	_month = dd._month;
	_day = dd._day;
}//引用传参

浅拷贝和深拷贝

浅拷贝

根据上面的内容。我们成功构建了Date类的拷贝构造函数,接下来我们调试以上修改好的程序

image-20241021102405748

成功把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;
}

image-20241021103339389

编译一下是显示没有语法错误的,然鹅运行程序时却

image-20241021103749835

为什么会出现错误呢?最主要的原因是拷贝函数只进行了浅拷贝也叫值拷贝,没有进行深度拷贝也叫深拷贝

原因剖析:

创建好q1对象后,继续执行拷贝构造q2对象

image-20241021105041657

已经执行完毕拷贝构造,当继续执行时会自动调用q1的析构函数,也就是Stack pushStack pop的析构函数~Stack,此时q2是浅拷贝,也就是q2里面的push 和pop类的地址与q1是一样的,当出了q1的作用域第一次调用析构函数,当出了q2的作用域会第二次调用析构函数。从而导致了释放两次_array的空间导致报错。

简单来说就是当 q1q2 退出作用域时,q1q2 中的 Stack 成员 pushpop 会调用各自的析构函数。由于它们共用了相同的 _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;
}
  1. 拷贝构造函数不会修改原对象(q1)

    • 在调用拷贝构造函数时,q1 的数据只会被读取,不会被修改,也不会重新分配内存。
  2. 新对象(q2)有自己独立的内存

    • q2 在拷贝构造函数中通过 malloc 分配了一块新的内存,并将 q1 的数据复制到这块内存中,因此 q1q2 的内存是独立的。
  3. 原对象(q1)的内存地址不变

    • 在整个拷贝过程中,q1_array 地址不会发生变化,因为拷贝构造函数只为新对象分配内存。
  4. malloc 执行两次,但只针对不同对象

    • q1 在构造时执行了一次 mallocq2 在拷贝构造时为自己执行了一次 malloc,这两次分配是独立的,互不影响。

因此,拷贝函数是一个特殊的函数,并不会改变原对象的内容,而仅仅是作拷贝用。调试程序,可以看见q1q2_array的地址是不一样的,是两块独立的内存空间。而这个过程中q1_array是不变的

image-20241021112251003

总结

**浅拷贝:**浅拷贝只复制对象的基本属性和指针,而不复制指针所指向的实际数据。这意味着源对象和目标对象中的指针会指向同一块内存。

特点:快速、节省内存。

可能导致问题:当一个对象被销毁时,它的指针所指向的内存也会被释放,另一个对象也会因为释放导致无效。

适用情况:适合于没有动态内存分配或者不需要独立对象的情况。例如Date类只有内置类型的类

深拷贝:深拷贝会复制对象及其所指向的所有数据,包括指针指向的内容。这意味着每个对象都有自己独立的内存。

特点:比浅拷贝更耗费时间和内存,因为需要为每个指针分配新的内存并复制数据。
避免了悬空指针的问题,因为每个对象都持有自己的数据副本。

适用情况:适合于含有动态内存分配的对象,或需要独立副本的情况。例如队列、栈、二叉树等需要开辟空间的数据结构。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吃椰子不吐壳

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值