首页 > 编程语言 >7.C++拷贝构造函数

7.C++拷贝构造函数

时间:2022-12-04 19:57:48浏览次数:44  
标签:Widget int 构造 C++ Time 拷贝 构造函数

拷贝构造函数

我们经常会用一个变量去初始化一个同类型的变量,那么对于自定义的类型也应该有类似的操作,那么创建对象时如何使用一个已经存在的对象去创建另一个与之相同的对象呢?

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

拷贝构造函数是构造函数的一个重载,因此显式的定义了拷贝构造,那么编译器也不再默认生成构造函数。

特征

拷贝构造也是一个特殊的成员函数。

特征如下:

  • 拷贝构造是构造函数的一个重载;
  • 拷贝构造的参数只有一个并且类型必须是该类的引用,而不是使用传值调用,否则会无限递归;
  • 若没有显式定义拷贝构造函数,编译器会自己生成一个默认拷贝构造,默认的拷贝构造函数对象按内存存储和字节序完成拷贝,也叫浅拷贝;
class Date
{
public:
	Date(int year, int month, int day)
		:
		_year(year),
		_month(month),
		_day(day)
	{}
	void Display()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2001, 7, 28);
	Date d2(d1);
	d1.Display();
	d2.Display();
	return 0;
}

输出:

2001-7-28

2001-7-28

  • 那么对于那些直接管理着内存资源的类(含有指针变量),那么简单的值拷贝还顶得住吗?显然顶不住啊。

通过图示说明:

image-20220521142159690

两个string类的对象指向了同一块空间,这不就乱套了吗,如果其中一个对象通过指针改变了指向内存的数据,那么另一个对象也会受到影响,这是我们不愿发生的,我们希望每个对象都能独立运作。

下面这个程序会崩溃。

class String
{
public:
	String(const char* str = "songxin")
	{
		cout << "String(const char* str = \"songxin\")" << endl;
		_str = (char*)malloc(strlen(str) + 1);
		strcpy(_str, str);
	}
	~String()
	{
		cout << "~String()" << endl;
		free(_str);
		_str = nullptr;
	}
private:
	char* _str;
};
int main()
{
	String s1;
	String s2(s1);
	return 0;
}

原因是两个string类的成员指针都指向一块内存,而它们又分别调用了一次析构函数,相当于对同一块内存空间释放了两次,程序崩溃。

因此对于这种情况的对象,我们就不能再使用编译器生成的默认拷贝构造了,而只能自己去显式的定义拷贝构造并且要实现深拷贝。

编译器生成的拷贝构造

编译器默认生成的拷贝构造会做些什么呢?

  • 对于内置类型成员

​ 完成值拷贝;

  • 对于自定义类型成员

    调用成员的拷贝构造;

class Time
{
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		:
		_hour(hour),
		_minute(minute),
		_second(second)
	{}
	Time(Time& t)
	{
		_hour = t._hour;
		_minute = t._minute;
		_second = t._second;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
Time top(0, 1, 1);
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1, Time& t = top)
		:
		_year(year),
		_month(month),
		_day(day),
		_t(t)
	{}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};
int main()
{
	Time t(1, 1, 1);
	Date d1(2001, 7, 28,t);
	Date d2(d1);
	return 0;
}

如果默认生成的拷贝构造没有调用Time类成员的拷贝构造,那么d2的_t的值应该是(_hour = 0, _minute = 0, _second = 0),而这里最终的结果是d2中的_t和d2中的_t值相同。

这里Date类中自动生成的拷贝构造函数的内置类型会进行字节序拷贝,而对于自定义类型_t调用了Time的拷贝构造函数。

拷贝构造的初始化列表

拷贝构造是构造函数的一个重载,因此拷贝构造函数也是有初始化列表的,所以也建议在初始化列表阶段完成对对象的初始化,养成良好习惯。

可以不显式定义拷贝构造函数的情况

  • 成员变量没有指针;
  • 成员有指针,但并没有管理内存资源;

显式定义拷贝构造的误区

之前一直存在这个误区:

我们都知道,编译器生成的构造函数在初始化列表会调用成员的构造函数,而我们显式去定义构造函数时,即使我们不写也会在初始化列表去调用自定义类型成员的构造函数。

通过类比,我就犯了一个低级错误:

就是既然编译器生成的拷贝构造可以在初始化列表自动调用自定义成员的拷贝构造,那么我们显式定义的拷贝构造即使不写,也会在初始化列表自动去调用自定义成员的拷贝构造。

于是我写出了如下代码:

class Time
{
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		:
		_hour(hour),
		_minute(minute),
		_second(second)
	{}
	Time(Time& t)
	{
		_hour = t._hour;
		_minute = t._minute;
		_second = t._second;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
Time top(2, 2, 2);
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1, Time& t = top)
		:
		_year(year),
		_month(month),
		_day(day),
		_t(t)
	{}
	Date(Date& d)//显式定义了拷贝构造
	{

	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};
int main()
{
	Time t(1, 1, 1);
	Date d1(2001, 7, 28,t);
	Date d2(d1);
	return 0;
}

通过监视窗口查看d2调用拷贝构造后的值:

image-20220523142121481

并没有拷贝成功。

我只顾着类比它们的功能,可我恰恰忽略了拷贝构造也是一种构造函数啊,那么自然初始化列表也是和普通构造一样,会去调用自定义类的构造函数,不处理内置类型。只不过编译器生成的是经过处理的构造函数达到了拷贝的效果。(太傻逼了这错误)

结论

拷贝构造函数是构造函数的一种,它也有初始化列表,如果是编译器生成的拷贝构造,它会对内置类型做字节序拷贝对自定义类型成员会调用自定义成员的拷贝构造

可如果是我们显式定义出的拷贝构造,它也是有初始化列表的,但是它的初始化列表可不会去调用成员的拷贝构造奥,而是和普通构造函数一样,对于内置类型成员不去初始化值,对于自定义类型成员调用自定义成员的构造函数而不是拷贝构造函数。

编译器关于拷贝构造的优化

下面这段代码共调用了多少次拷贝构造?

class Widget
{
public:
	Widget()
		:
		_n()
	{
		cout << "Widget()" << endl;
	}
	Widget(const Widget& d)
		:
		_n(d._n)
	{
		cout << "Widget(Widget& d)" << endl;
	}
private:
	int _n;
};

Widget f(Widget u)
{
	Widget v(u);
	Widget w = v;
	return w;
}

int main() 
{
	Widget x;
	Widget y = f(f(x));
}

我们一个一个数。

首先执行f(x),那么会传参拷贝(第一次)

函数体Widget v(u);(第二次)

Widget w = v;(第三次)

return w;用w拷贝构造一个的临时对象tmp(第四次),临时对象tmp作为实参传值拷贝给形参u(第五次)

函数体Widget v(u);(第六次)

Widget w = v;(第七次)

return w;用w拷贝构造一个的临时对象tmp(第八次),使用临时对象tmp拷贝构造y(第九次)

以上是我们的分析结果,但事实如此吗?

输出结果:

Widget()
Widget(Widget& d)
Widget(Widget& d)
Widget(Widget& d)
Widget(Widget& d)
Widget(Widget& d)
Widget(Widget& d)
Widget(Widget& d)

这里调用了七次拷贝构造,和我们分析出的不太一样。

我们知道不同编译器是有差异的,它们和C++标准是有一些出入的,在一些比较新的编译器通常会做下列优化,让我们的程序更快更节约资源。

在一个表达式中,连续构造会被优化:

  • 构造+拷贝构造
class Widget
{
public:
	Widget(int n = 0)
		:
		_n(n)
	{
		cout << "Widget()" << endl;
	}
	Widget(const Widget& d)
		:
		_n(d._n)
	{
		cout << "Widget(Widget& d)" << endl;
	}
private:
	int _n;
};
int main() 
{
	Widget x = 1;
}

输出:

Widget()

按照以前所讲的,第29行这里会发生隐式类型转换,即会用1去调用Widget的构造函数,并且1会作为构造函数的第一个形参,构造出一个Widget临时对象再使用这个临时对象去调用x的拷贝构造,简言之就是先构造再拷贝构造

但是这里通过输出我们知道Widget x = 1;只调用了一次构造函数,也就是说这种先构造再拷贝构造会被编译器优化为直接使用1去构造x,而不是用1构造一个临时对象,再使用临时对象去拷贝构造x。

  • 拷贝构造+拷贝构造
class Widget
{
public:
	Widget(int n = 0)
		:
		_n(n)
	{
		cout << "Widget()" << endl;
	}
	Widget(const Widget& d)
		:
		_n(d._n)
	{
		cout << "Widget(Widget& d)" << endl;
	}
private:
	int _n;
};

Widget f(Widget u)
{
	Widget v(u);
	Widget w = v;
	return w;
}

int main()
{
	Widget x;
	Widget y = f(x);
	return 0;
}

输出:

Widget()
Widget(Widget& d)
Widget(Widget& d)
Widget(Widget& d)
Widget(Widget& d)

调用了四次拷贝构造。

倘若我们将30行改为f(x);,再来看看输出结果:

Widget()
Widget(Widget& d)
Widget(Widget& d)
Widget(Widget& d)
Widget(Widget& d)

这两次居然是一样的,可明明Widget y = f(x);应该比f(x);要多一次拷贝构造啊???毕竟多了一步y的拷贝构造。

说明其中有两次拷贝构造被合并为一次拷贝构造了,传参过程和函数体内中是一定不会存在拷贝构造的优化的,只有return w;,会先使用w拷贝构造一个临时对象,再用临时对象去拷贝构造y这段过程会被优化,这两次拷贝构造被优化为直接使用w去拷贝构造y,因此这两段代码调用拷贝构造的次数是相同的。

结论:

  1. 构造 + 拷贝构造会被编译器优化为一次构造;
  2. 连续的拷贝构造会被编译器优化为一次拷贝构造;

因此为何第一段代码我们也就理解了,第四次和第五次会合并为一次,第八次和第九次会合并为一次,总共调用了七次拷贝构造。

关于这里的优化不是C++标准所规定的,了解即可。

标签:Widget,int,构造,C++,Time,拷贝,构造函数
From: https://www.cnblogs.com/ncphoton/p/16950517.html

相关文章

  • 6.C++构造函数
    类的6个默认成员函数如果我们写了一个类,这个类我们只写了成员变量没有定义成员函数,那么这个类中就没有函数了吗?并不是的,在我们定义类时即使我们没有写任何成员函数,编译器......
  • 5.C++类和对象(上)
    面向过程和面向对象初步认识C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。C++是基于面向对象的,关注的是对象,将一件事拆分成不同的对象......
  • 11.C++日期类的实现
    日期类的实现在前面学过默认成员函数后,我们就可以写一个简单的日期类了。如何写呢?我们可以先分析分析。日期类的成员变量都是int类型,那么构造函数是要显式定义的,成员变......
  • 10.C++类和对象(下)
    再谈构造函数之前讲过构造函数的一些特性,再在这里补充下。构造函数体赋值classDate{public: Date(intyear,intmonth,intday) { _year=year; _month=m......
  • 9.C++运算符重载
    运算符重载本文包括了对C++类的6个默认成员函数中的赋值运算符重载和取地址和const对象取地址操作符的重载。运算符是程序中最最常见的操作,例如对于内置类型的赋值我们直......
  • 8.C++析构函数
    析构函数既然在创建对象时有构造函数(给成员初始化),那么在销毁对象时应该还有一个清除成员变量数据的操作咯。概念析构函数:与构造函数功能相反,析构函数不是完成对象的销......
  • 13.C++模板初阶
    泛型编程如何实现一个通用的交换函数呢?voidSwap(int&left,int&right){ inttemp=left; left=right; right=temp;}voidSwap(double&left,double&ri......
  • 12.C++内存管理
    在C语言的学习中我们已经接触过内存管理,那么C++与C语言之间又有什么不同和相同的地方呢?C++内存分布intglobalVar=1;staticintstaticGlobalVar=1;voidTest(){......
  • C++图书管理系统(管理员-读者)
    C++图书管理系统(管理员-读者)一、设计一款文字式交互的图书管理系统,要求具备注册登录、浏览图书、借还图书等基本功能;二、要求以外部文件的形式存储书籍信息、馆藏记录、......
  • 关于python深拷贝,deepcopy和 copy的知识随手记
    Python中copy模块下的deepcopy函数使用,采用的深层拷贝,并开辟新的空间   如果用copy函数,  如果拷贝的是不可变类型: ......