C++类和对象进阶:拷贝构造函数深度详解

拷贝构造函数

在这里插入图片描述

前言

上文中详解了构造函数析构函数,本文详解类的六个默认成员函数中的第三个拷贝构造函数,并辨析区分深拷贝与浅拷贝

引入

在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。
在这里插入图片描述
那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?

答案当然是有的, 拷贝构造函数就是来完成这一工作的。

拷贝构造函数

拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用

特征

  1. 拷贝构造函数是构造函数的一个重载形式。
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用
//拷贝构造函数实例
class Date {
    
    
private:
	int _year;
	int _month;
	int _day;
public:
	Date(int year = 2024, int month = 10, int day = 28) {
    
    
		_year = year;
		_month = month;
		_day = day;
	}
	//拷贝构造函数实例
	Date(const Date& date) {
    
    	
		_year = date._year;
		_month = date._month;
		_day = date._day;
	}
};

int main(){
    
    
	Date d1(2025, 2, 8);
	Date d2(d1);	//调用拷贝构造函数,用已有对象初始化新的对象
}
拷贝构造函数建议参数加上const
Date(const Date& date) {
    
    	
	//const 是为了防止别人写错,防止别人写成以下代码
	date._year = _year;		//不仔细看真看不出来,赔了夫人又折兵,会出现随机值
	date._month = _month;
	date._day = _day;
}

在这里插入图片描述

  • 不加const可能会出现随机值。

拷贝构造函数参数传值会引发无穷递归的解释

解释前,我们需要明晰该概念: 函数调用前,需要先传参。
**C++**中传参时有如下要求:

  1. 内置类型,无要求,直接拷贝
  2. 自定义类型,值传参,必须调用其拷贝构造函数
    在这里插入图片描述
内置类型传参拷贝

C++内置类型拷贝

我们可以看到,

自定义类型传参拷贝

C++自定义类型拷贝

我们可以看到:

  • 内置类型作为参数时,调用函数会直接进入函数内。。
  • 自定义类型作为参数时,调用函数,会先调用其拷贝构造函数来拷贝。

此时再来看,若拷贝构造函数为值传递:

Date(const Date date) {
    
    		//下面解释会无穷递归调用
	_year = date._year;
	_month = date._month;
	_day = date._day;
}
详细解释

栈帧基本概念

  • 栈帧(Stack Frame): 当一个函数被调用时,程序会为这个函数分配一个栈帧,其中存放了函数的参数、局部变量、返回地址等信息
  • 函数调用和返回: 每当一个函数调用完成后,其对应的栈帧会被释放。如果函数不断调用自身(递归),每次调用都会在栈上分配一个新的栈帧。如果没有合适的终止条件,栈帧会不断累积,最终导致栈空间耗尽(栈溢出)。

在这里插入图片描述
执行语句Date d2(d1)时,流程如下:

  1. Date d2(d1),执行该语句,参数列表匹配,会调用其拷贝构造函数,构造函数会创建一个栈帧,栈帧内空间存放有形参date
  2. 形参date实参d1的一份拷贝,拷贝时会调用Date的拷贝构造函数,而拷贝构造函数Date(const Date date)参数为自定义类型的值传递,自定义类型的值传递,同样会创建栈帧并调用其拷贝构造函数
  3. 拷贝构造函数继续传值引发对象的拷贝,之后会层层传值引发对象的拷贝的递归调用

总结调用 Date d2(d1)时,新分配一个拷贝构造函数的栈帧,同时栈帧内创建参数的副本创建参数的副本时又不断分配新的栈帧,层层嵌套,无法返回,直至栈空间耗尽。

编译器生成的默认拷贝构造函数

若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

默认构造函数做了什么?
  • 内置类型成员完成,,值拷贝/浅拷贝
  • 自定义类型,调用各自的拷贝构造函数,如果自定义类型没有拷贝构造函数,则值拷贝
//默认拷贝构造函数
class Date_1 {
    
    
private:
	int _year;
	int _month;
	int _day;
public:
	Date_1(int year = 2024, int month = 10, int day = 28) {
    
    
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}
	/*Date_1(const Date_1& date) {
		this->_year = date._year;
		this->_month = date._month;
		this->_day = date._day;
	}*/
};
int main() {
    
    
	Date_1 d1(2025, 6, 6);
	Date_1 d2(d1);
	return 0;
}

在这里插入图片描述
可以看到,编译器自动生成的默认拷贝构造函数,可以完成Date_1(类内均为自定义类型)的拷贝。

深拷贝与浅拷贝

再看如下代码:

class Stack {
    
    
private:
	int* _base = nullptr;	//C++11支持的成员变量缺省值
	int _top = 0;
	int _capacity = 0;
public:
	Stack(int defaultCapacity = 4) {
    
    
		this->_base = (int*)malloc(sizeof(int) * defaultCapacity);
		if (this->_base == nullptr) {
    
    
			perror("malloc failed\n");
			return;
		}
		this->_capacity = defaultCapacity;
		this->_top = 0;
	}
	~Stack() {
    
    
		cout << " ~Stack" << endl;
		free(this->_base);
		this->_base = nullptr;
		this->_capacity = 0;
		this->_top = 0;
	}
};
int main(){
    
    
	Stack st1;
	Stack st2(st1);
	return 0;	
}

运行结果如下:
在这里插入图片描述
这是为什么呢?原因就是浅拷贝的危害

在这里插入图片描述

  • 值拷贝(浅拷贝)存在问题,值拷贝时,由于只是简单的值拷贝,导致两个栈类对象中,两个指针存下了同一块空间的地址
    在这里插入图片描述

  • 析构函数完成的是对象中资源空间的清理和释放。可以看到,当两个栈对象的生命周期结束时,析构函数会调用了两次,那也就是说,会对同一块空间析构(释放)两次,这是这便是引发错误的原因。

  • 隐患,由于两块空间的地址相同,我们对对象st1进行push操作时,也会改变对象st2中的值,这并不是我们所希望的。因此我们应该在此类场景中避免浅拷贝。

此类对象的拷贝构造函数需要实现深拷贝!

简单实现一个深拷贝。
//在拷贝构造函数中简单实现一个深拷贝  st2(st1)
Stack(const Stack& stack) {
    
    
	this->_base = (int*)malloc(sizeof(int) * stack._capacity);
	if (this->_base == nullptr) {
    
    
		perror("malloc failed\n");
		return;
	}
	memcpy(this->_base, stack._base, sizeof(int) * stack._top);
	this->_top = stack._top;
	this->_capacity = stack._capacity;
}

在这里插入图片描述
可以看到,实现深拷贝后,程序正常返回。

深浅拷贝总结

  • 类中如果没有涉及资源申请时,拷贝构造函数是否写都可以主要是要防止浅拷贝后,两个对象内的成员指针指向同一块空间
  • 一旦涉及到资源申请时,则拷贝构造函数是一定要写的,且要实现深拷贝,否则就是浅拷贝。

拷贝构造函数典型使用场景

拷贝构造函数典型调用场景:

使用已存在的对象创建新对象。

Date d1(2025, 2, 11);
Date d2(d1);	//调用拷贝构造函数
  • 其作用是通过深拷贝或浅拷贝初始化一个新对象为已有对象的副本。。

函数参数类型为类类型对象。

Date Test(Date d);	//自定义类型传参时,必须调用其拷贝构造
  • 以值方式传递参数时,会调用一次拷贝构造函数创建形参d,存放在函数栈帧中

函数返回值类型为类类型对象。

Date Test(Date d) {
    
    
	Date temp(d);	//使用已存在对象创建新对象
	return temp;	//返回式
}
  • 函数返回temp时,若未启用返回值优化(RVO),会调用拷贝构造函数生成临时对象

总结需要实现拷贝构造的场景

声明:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
因此:

  • 类中如果没有涉及资源申请时(如堆区的内存),拷贝构造函数是否写都可以
  • 一旦涉及到资源申请时(开辟了堆区的空间),则拷贝构造函数是一定要写的,否则就是浅拷贝

文章到此结束啦,以上便是要介绍的关于拷贝构造函数的所有内容。欢迎各位大佬在评论区讨论交流,如果觉得文章写的不错,还请留下免费的赞和收藏

猜你喜欢

转载自blog.csdn.net/2301_80064645/article/details/145593384
今日推荐