C++_拷贝构造函数

2023-12-19 22:55:25

? ? ? ?

目录

1、拷贝构造的实现

2、传值方式接收实参的影响

3、const修饰形参别名

4、传值返回的拷贝构造

?4.1 传引用返回的拷贝构造

5、默认拷贝构造函数

5.1 对内置类型的拷贝

5.1 对自定义类型的拷贝

结语:


前言:

????????拷贝构造函数的作用是实现对象的拷贝,通常是把一个已经存在的对象的成员变量拷贝给一个刚被实例化出来的对象,以拷贝的形式完成对该对象的初始化。因此拷贝构造函数其实是构造函数的一种重载的形式,而且进行对象拷贝时由系统自动调用拷贝构造函数。

1、拷贝构造的实现

? ? ? ? 拷贝构造函数的函数名与类名相同,且没有返回类型,函数只有一个形参,通常会把形参的类型设为该类类型的一个引用,表示形参为实参的一个”别名“,具体写法如下:

#include<iostream>
using namespace std;

class Date//创建一个Date的类
{
public:

	Date(int year = 1, int month = 1, int day = 1)//构造函数
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Date(Date& d)//拷贝构造
	{
        cout << "Date(Date& d)" << endl;//观察是否进入拷贝构造
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	void Print()//打印
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date dt1(2022, 2, 22);//初始化dt1
	Date dt2 = dt1;//把dt1的内容拷贝给dt2,完成对dt2的初始化
    //Date dt2(dt1);//该写法也可以实现dt1拷贝到dt2
	dt1.Print();
	dt2.Print();
	return 0;
}

? ? ? ? 运行结果:

? ? ? ? 从结果可以看到完成了对象的拷贝。在执行到语句Date dt2 = dt1时,会跳到拷贝构造函数处完成将dt1的成员变量的值拷贝给dt2的成员变量这一动作。

? ? ? ? 具体示意图如下:

2、传值方式接收实参的影响

? ? ? ? 拷贝构造函数中形参的类型是一个引用,因此是用传引用的方式来接收对象,那为何不用传值的方式来接收对象呢?原因在于若用传值方式会导致类似无限递归的错误,因为传值方式的形参是实参的一份临时拷贝,所以形参会先将实参的内容拷贝一份,这时候又涉及到拷贝构造,因此又进入拷贝构造进行拷贝,结果就是进不去函数的内部,在函数的形参部分无限递归。

? ? ? ? 示意图如下:

3、const修饰形参别名

? ? ? ? 从上文得知,拷贝构造函数的形参表示的是被拷贝对象(dt1)的别名,所以拷贝构造函数目的是修改拷贝对象(dt2)的值,那么被拷贝对象其实是不能够被修改的,所以一般被拷贝对象的别名都会用const进行修饰,以防被拷贝对象被修改。

Date(Date& d)//拷贝构造
	{
		cout << "Date(Date& d)" << endl;//观察是否进入拷贝构造
		/*_year = d._year;
		_month = d._month;
		_day = d._day;*/

        //不加const则被拷贝对象有被修改的风险
		d._year = _year;
		d._month = _month;
		d._day = _day;
	}

? ? ? ? 被拷贝对象被修改的结果:

? ? ? ? 所以拷贝构造函数的形参一般都需要用const进行修饰,这样就可以避免被拷贝对象被修改。

4、传值返回的拷贝构造

? ? ? ? 上文提到,如果把对象当作实参传给函数的形参,形参用传值方式来接收,那么会调用拷贝构造函数,还有一种场景也会调用拷贝构造函数,即函数返回值采用的是传值返回的方式,那么也会进入拷贝构造。因为传值是返回一个对象的拷贝,所以需要调用拷贝构造函数。

? ? ? ? 示例代码如下:

#include<iostream>
using namespace std;

class Date//创建一个Date的类
{
public:

	Date(int year = 1, int month = 1, int day = 1)//构造函数
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Date(const Date& d)//拷贝构造
	{
		cout << "Date(Date& d)" << endl;//观察是否进入拷贝构造
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	Date OneMonth_late()//只是为了演示拷贝函数而定义的函数
	{
		Date temp = *this;//第一次进入拷贝构造
		temp._month++;
		if (temp._month > 12)
		{
			temp._year++;
			temp._month = 1;
		}
		return temp;//传值返回,第二次进入拷贝构造
	}

	void Print()//打印
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date dt1(2022, 12, 22);
	Date dt2 = dt1.OneMonth_late();//将一个月之后的日期赋给dt2
	dt2.Print();
    dt1.Print();
	return 0;
}

? ? ? ? 运行结果:

? ? ? ? 从结果可以看到,总共进入了两次拷贝构造函数。具体示意图如下:

?4.1 传引用返回的拷贝构造

? ? ? ? 传引用返回表示返回一个“别名”给到外部,不再是返回一个对象的拷贝给到外面,因此传引用返回不需要调用拷贝构造函数,只需要在外部给dt2拷贝的时候进行一次拷贝构造即可。

? ? ? ? 注意:使用传引用返回的时候,对象必须出了函数不会被销毁,上述temp出了作用域会被销毁,因此不能用传引用返回,不过传值返回的好处在于不会修改dt1的值,而传引用返回会改变dt1原本的值。

#include<iostream>
using namespace std;

class Date//创建一个Date的类
{
public:

	Date(int year = 1, int month = 1, int day = 1)//构造函数
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Date(const Date& d)//拷贝构造
	{
		cout << "Date(Date& d)" << endl;//观察是否进入拷贝构造
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	Date& OneMonth_late()//只是为了演示拷贝函数而定义的函数
	{
		_month++;
		if (_month > 12)
		{
			_year++;
			_month = 1;
		}
		return *this;//传引用返回,无需调用拷贝函数
	}

	void Print()//打印
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date dt1(2022, 12, 22);
	Date dt2 = dt1.OneMonth_late();//此处给dt2拷贝的时候需调用一次拷贝构造函数
	dt2.Print();
	return 0;
}

? ? ? ? 运行结果:

? ? ? ? ?从结果看到,只调用了一次拷贝构造。具体示意图如下:

5、默认拷贝构造函数

5.1 对内置类型的拷贝

? ? ? ? 如果我们自己不写拷贝构造函数,那么编译器也会自动生成一个拷贝构造函数,称为默认拷贝构造函数,该默认拷贝构造函数对内置类型的成员变量会按一个字节一个字节的拷贝。(内置类型包括int、char、double....以及各种类型的指针)

? ? ? ? 体现默认拷贝构造函数的代码如下:

#include<iostream>
using namespace std;

class Date//创建一个Date的类
{
public:

	Date(int year = 1, int month = 1, int day = 1)//构造函数
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()//打印
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date dt1(2022, 12, 22);
	Date dt2 = dt1;//没有定义拷贝构造函数
	dt2.Print();

	return 0;
}

? ? ? ? ?运行结果:

? ? ? ? 从结果可以看到,尽管我们没有定义拷贝构造函数,但是依然可以完成拷贝工作,?原因就是系统调用了自己生成的拷贝构造函数完成了拷贝任务。


? ? ? ? 当对象的成员变量涉及到空间资源的问题(如成员变量是指针类型,并且申请了空间),则不能调用默认拷贝构造函数了,因为我们期望拷贝是重新开辟一块新的空间给到拷贝对象的成员指针,而默认拷贝构造函数会把被拷贝对象的成员指针指向的空间地址给到拷贝对象的成员指针(只是将成员指针保存的地址拷贝过去),而不是从新开辟一块新的空间给到拷贝对象,这就会导致两个对象的成员指针指向同一块空间,就不是我们所期望的了。

? ? ? ? 示意图如下:

? ? ? ? 所以面对涉及空间资源的成员变量,则需要我们手动写拷贝构造函数,代码如下:

#include<iostream>
using namespace std;

class Stack//用模拟栈来举例
{
public:

	Stack(int n=4)//构造函数
	{
		cout << "Stack()" << endl;//观察调用了几次构造函数
		_arr = (int*)malloc(sizeof(int) * n);
		if (_arr == nullptr)
		{
			perror("malloc");
			return;
		}
		_top = 0;
		_capacity = n;
	}

	Stack(Stack& st)//拷贝构造函数
	{
        cout << "Stack(Stack& st)" << endl;//观察调用了几次拷贝构造函数
		_arr = (int*)malloc(sizeof(int) * st._capacity);//新开辟一块空间
		if (_arr == nullptr)
		{
			perror("malloc");
			return;
		}
		memcpy(_arr, st._arr, sizeof(int) * st._top);//拷贝栈里的内容
		_top = st._top;
		_capacity = st._capacity;
	}

	void push(int x)//压栈操作
	{
		_arr[_top++] = x;
	}

	~Stack()//析构函数
	{
		cout << "~Stack()" << endl;//观察调用了几次析构函数
		free(_arr);
		_arr = nullptr;
		_top = _capacity = 0;
	}

private:
	int* _arr;
	int _top;
	int _capacity;
};

int main()
{
	Stack st1;
	st1.push(2);
	st1.push(3);//此时st1中的_arr的栈顶元素为3
	Stack st2 = st1;
	return 0;
}

? ? ? ? 运行结果:

? ? ? ? 首先程序可以正常运行,从结果可以看到,调用了一次构造函数进行初始化,一次拷贝构造函数完成对st2的拷贝,最后两次析构函数完成对申请空间的资源释放。

? ? ? ? 通过调试观察st2中的成员变量和st1是否相同:

? ? ? ? 可以发现st1中的_arr所指向的空间的地址和st2中的_arr所指向的空间的地址是不一样的,意味着拷贝构造函数没问题,确实新开了空间给到st2,并且把st1中的内容拷贝到了st2中。

5.1 对自定义类型的拷贝

? ? ? ? 默认拷贝构造函数对自定义类型的成员变量会去调用该成员变量的拷贝构造函数,所以对于自定义类型的成员变量不写拷贝构造函数也可以,前提是该成员变量自己的拷贝构造函数可以满足同类型的对象拷贝。

? ? ? ? 将上述代码进行变形得到示例代码如下:

#include<iostream>
using namespace std;

class Stack//用模拟栈来举例
{
public:

	Stack(int n=4)//构造函数
	{
		cout << "Stack()" << endl;//观察调用了几次构造函数
		_arr = (int*)malloc(sizeof(int) * n);
		if (_arr == nullptr)
		{
			perror("malloc");
			return;
		}
		_top = 0;
		_capacity = n;
	}

	Stack(Stack& st)//拷贝构造函数
	{
		cout << "Stack(Stack& st)" << endl;//观察调用了几次拷贝构造函数
		_arr = (int*)malloc(sizeof(int) * st._capacity);//新开辟一块空间
		if (_arr == nullptr)
		{
			perror("malloc");
			return;
		}
		memcpy(_arr, st._arr, sizeof(int) * st._top);//拷贝栈里的内容
		_top = st._top;
		_capacity = st._capacity;
	}

	void push(int x)//压栈操作
	{
		_arr[_top++] = x;
	}

	~Stack()//析构函数
	{
		cout << "~Stack()" << endl;//观察调用了几次析构函数
		free(_arr);
		_arr = nullptr;
		_top = _capacity = 0;
	}

private:
	int* _arr;
	int _top;
	int _capacity;
};

class DoubleStack//用于测试成员变量类型为自定义类型的默认拷贝构造
{
public:
	Stack st1;
};

int main()
{
	DoubleStack dst1;
	dst1.st1.push(1021);//此时st1中的_arr的栈底元素为1021
	dst1.st1.push(2);
	DoubleStack dst2 = dst1;
	
	return 0;
}

? ? ? ? 通过调试可以看到dst2和dst1中的内容如下:

? ? ? ? 可以看到dst2和dst1中栈底元素都是1021,而且dst2和dst1中的_arr都指向的是不同的区域,说明默认拷贝构造函数对自定义类型的成员变量会去调用他们自己的拷贝构造函数,完成了dst1对dst2的拷贝。

结语:

? ? ? ? 以上就是关于拷贝构造函数的讲解,拷贝构造函数只需要记住涉及到空间资源的情况需要自己写拷贝构造函数,其他情况系统自动生成的拷贝构造就足够完成需求了,并且在传值返回和形参以传值方式接收的情况下会进行拷贝构造。最后希望本文可以给你带来更多的收获,如果本文对你起到了帮助,希望可以动动小指头帮忙点赞👍+关注😎+收藏👌!如果有遗漏或者有误的地方欢迎大家在评论区补充~!谢谢大家!!( ̄︶ ̄)↗ 

文章来源:https://blog.csdn.net/efls111/article/details/135082559
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。