拷贝构造函数是一种特殊的构造函数,具有一般构造函数的所有特性,其形参是本类的对象的引用。其作用是使用一个已经存在的对象(由拷贝构造函数的参数指定),去初始化同类的一个新对象。
如果程序员没有定义类的拷贝构造函数,系统会在必要时自动生成一个隐含的拷贝构造函数。这个隐含的拷贝构造函数的功能是:把初始值对象的每个数据成员的值都拷贝到新建立的对象中。这样就完成了同类对象的拷贝,这样得到的对象和原对象具有完全相同的数据成员,即完全相同的属性。
声明和实现拷贝构造函数的一般方法:

class 类名
{
public:
	类名(形参表);//构造函数
	类名(类名&对象名);//拷贝构造函数
	...
};
类名::类名(类名&对象名)//拷贝构造函数的实现
{
	函数体
}

【例】
通过水平和垂直两个方向的坐标值X和Y类来确定屏幕上的一个点。点Point类的定义如下:

class Point
{
publicPoint(int xx=0,int yy=0)
	{
		x=xx;
		y=yy;
	}
	Point(Point&p);
	int getX()
	{
		return x;
	}
	int getY()
	{
		return y;
	}
private:
	int x,y;
};
Point::Point(Point&p)
{
	x=p.x;
	y=p.y;
	cout<<"调用拷贝构造函数"<<endl;
}

类中声明了内联构造函数和拷贝构造函数。
拷贝构造函数的实现如下:

Point::Point(Point& p)
{
	x = p.x;
	y = p.y;
	cout << "调用拷贝构造函数" << endl;
}

普通构造函数是在对象被创建的时候调用,而构造函数在以下三种情况下会被调用:

1.当用类的一个对象去初始化该类的另一个对象时

例如:

#include<iostream>
using namespace std;
class Point
{
public:
		Point(int xx = 0, int yy = 0):x(xx),y(yy)
	{

	}
	Point(Point& p);
	int getX()
	{
		return x;
	}
	int getY()
	{
		return y;
	}
private:
	int x, y;
};
Point::Point(Point& p):x(p.x),y(p.y)
{
	cout << "调用拷贝构造函数" << endl;
}
int main()
{
	Point a(1, 2);
	Point b(a);//用对象a初始化对象b,拷贝构造函数被调用
	cout << b.getX() << endl;
	Point c = a;//用对象a初始化对象c,拷贝构造函数被调用
	cout << c.getX() << endl;
	return 0;
}

运行结果:
在这里插入图片描述
【提示】以上对于b和c的初始化都调用了拷贝构造函数,这两种写法只是形式上不同,执行的操作完全相同。
【例】
类提供一个默认的拷贝构造函数,用已有对象数据成员的值去依次初始化新对象数据成员的值。
如果程序员提供了拷贝构造函数,类就不会提供默认的拷贝构造函数。

#include<iostream>
using namespace std;
class A
{
public:
	A(int i=0):m_i(i){}
	void Print()
	{
		cout << m_i << endl;
	}
private:
	int m_i;
};
void main()
{
	/*下面两句话所实现的功能相当于:int i=10;int j=i;*/
	A a(5);//用整型5去构造了一个对象a   //调用了构造函数A(int)
	A b(a);//用a对象去初始化b对象,b也是一个新对象,b的值由a来给,相当于用a去构造b

	/*用a去初始化b,相当于用a对象里面所有数据成员的值去初始化b对象里面的所有数据成员*/

	/*要想构造对象,就要去调用当前类中的构造函数,A类中没有与b匹配的构造函数*/
    
	/*但是程序可以运行,并且输出正确的结果,说明当前的类里面默认提供了一个构造函数*/
	a.Print();
	b.Print();
}

【分析】
用a去初始化b,相当于用a对象里面所有数据成员的值去初始化b对象里面的所有数据成员;
要想构造对象,就要去调用当前类中的构造函数,发现A类中没有与b匹配的构造函数;
但是程序可以运行,并且输出正确的结果,说明当前的类里面默认提供了一个构造函数。
运行结果:
在这里插入图片描述

那么这个类默认提供的拷贝构造函数的参数是什么类型呢?

(1)拷贝构造函数如果为值类类型的参数:
#include<iostream>
using namespace std;
class A
{
public:
	A(int i=0):m_i(i){}
	
	//A(A t){}//值类类型  error
	/*因为在传参的时候,a传给t的过程中又要调用构造函数,会产生递归调用,出不来结果 */
	void Print()
	{
		cout << m_i << endl;
	}
private:
	int m_i;
};
void main()
{
	A a(5);//用整型5去构造了一个对象a   //调用了构造函数A(int)
	//A b = a;
	A b(a);//用a对象去初始化b对象,b也是一个新对象,b的值由a来给,相当于用a去构造b
	a.Print();
	b.Print();
}

这样程序是错误的,编译不通过,因为在传参的时候,a传给t的过程中又要调用构造函数,会产生递归调用,出不来结果 。

(2)拷贝构造函数如果为指针类类型
#include<iostream>
using namespace std;
class A
{
public:
	A(int i=0):m_i(i){}
	A(A*t):m_i(t->m_i)
	//构造一个指针类类型的对象,就要传地址
	{
		cout << "A(A)" << endl;
	}
	//用指针类类型的拷贝构造函数  可以运行并输出正确的结果,初始化功能也可以实现
	void Print()
	{
		cout << m_i << endl;
	}
private:
	int m_i;
};
void main()
{
	A a(5);//用整型5去构造了一个对象a   //调用了构造函数A(int)
	//A b = a;
	//A b(a);//用a对象去初始化b对象,b也是一个新对象,b的值由a来给,相当于用a去构造b
	A b(&a);
	/*要构造一个指针类类型的对象,就要传地址,要用到传地址符&,用指针的话,这句话写成赋值形式就是A b = &a;
	这样就不知道到底是用a的地址去初始化b还是用a本身去初始化b,会产生视觉上的歧义,不建议使用*/
	a.Print();
	b.Print();
}

【分析】用指针类类型的拷贝构造函数 可以运行并输出正确的结果,初始化功能也可以实现。
但是要构造一个指针类类型的对象,就要传地址,要用到传地址符&,用指针的话,A b(&a);这句话写成赋值形式就是A b = &a;。这样就不知道到底是用a的地址去初始化b还是用a本身去初始化b,会产生视觉上的歧义,不建议使用。
运行结果:
在这里插入图片描述

(3)拷贝构造函数如果为引用类类型
#include<iostream>
using namespace std;
class A
{
public:
	A(int i=0):m_i(i){}
	//拷贝构造函数
	A(const A& t) :m_i(t.m_i)
		//不是把a传给t,而是把a重新叫了一个名字为t,t是a的一个别名
		//const 的作用:在初始化的时候并不修改a中数据成员的值
		//&:用a本身去给b构造
	{
		cout << "A(A)" << endl;
	}

	void Print()
	{
		cout << m_i << endl;
	}
private:
	int m_i;
};
void main()
{
	A a(5);//用整型5去构造了一个对象a   //调用了构造函数A(int)
	//A b = a;
	A b(a);//用a对象去初始化b对象,b也是一个新对象,b的值由a来给,相当于用a去构造b

	/*用a去初始化b,相当于用a对象里面所有数据成员的值去初始化b对象里面的所有数据成员*/

	/*要想构造对象,就要去调用当前类中的构造函数,A类中没有与b匹配的构造函数*/
    
	/*但是程序可以运行,并且输出正确的结果,说明当前的类里面默认提供了一个构造函数,这个构造函数参数里面的类型为引用类类型*/
	a.Print();
	b.Print();
}

【分析】
构造函数的参数为引用类类型,不是把a传给t,而是把a重新叫了一个名字为t,t是a的一个别名。
参数中const 的作用:在初始化的时候并不修改a中数据成员的值。
引用(&)的意义:用a本身去给b构造。

运行结果:
在这里插入图片描述

2.函数的形参是类的对象,是值类型的形参,调用函数时,进行形参和实参结合时。
#include<iostream>
using namespace std;
class Point
{
public:
		Point(int xx = 0, int yy = 0):x(xx),y(yy)
	{

	}
	Point(Point& p);
	int getX()
	{
		return x;
	}
	int getY()
	{
		return y;
	}
private:
	int x, y;
};
Point::Point(Point& p):x(p.x),y(p.y)
{
	cout << "调用拷贝构造函数" << endl;
}
void f(Point p)
{
	cout << p.getX() << endl;
}
int main()
{
	Point a(1, 2);
	Point b(a);//用对象a初始化对象b,拷贝构造函数被调用
	cout << b.getX() << endl;
	Point c = a;//用对象a初始化对象c,拷贝构造函数被调用
	cout << c.getX() << endl;
	f(a);//函数的形参为类的对象,调用函数时,拷贝构造函数被调用
	return 0;
}

运行结果:
在这里插入图片描述
函数传参为值传递的时候,由实参传递给形参,实参是旧对象,形参是新对象,所以要调用拷贝构造函数。
【例】

class A
{
public:
	A(int i = 0) :m_i(i)
	{
		cout << "A" << m_i << endl;
	}
	~A()
	{
		cout << "~A" << m_i << endl;
	}
	A(const A& t) :m_i(t.m_i)

	{
		cout << "A(A)" <<m_i<< endl;
	}
private:
	int m_i;
};
void fn(A s) //A s(c) 用c构造s,调用拷贝构造函数
//值类类型的参数
//形参在调用这个函数的时候才给形参开辟空间,没有调用函数之前形参是没有的,所以s是新的
{
	cout << "fn" << endl;
	//fn函数在即将退出的时候要将局部对象s析构,要调用析构函数,~A 30
}
void main()
{
	A a(5);//调用普通构造函数A(int),输出A5
	A b(a);//调用拷贝构造函数A(A),输出A5
	A c(30);//调用普通构造函数A(int),输出A30
	fn(c);
	//调用fn函数,第一步传参:将c对象传给对象s,
	//c对象是已有的旧对象,s是新对象,用c去构造s,调用拷贝构造函数A(A),输出A30
	/*在调用fn函数的时候才会给形参s开辟空间,也就是说把b传给s的时候才会给s开辟空间
	相当于s对象是新对象,b对象是实参,s对象是形参,从旧对象b到新对象s*/
	//不是把c本身传给s,把c的值拷贝了一份给s去进行初始化

	//再将要退出主函数的时候,要将c对象,b对象,a对象释放掉,析构c,b,a
	//~A 30  ~A 5  ~A 5
}

【分析】主函数中调用fn函数,第一步传参:将c对象传给对象s,c对象是实参,s对象是形参,在调用fn函数的时候才会给形参s开辟空间,也就是说把c传给s的时候才会给s开辟空间。c对象是已有的旧对象,s是新对象,从旧对象b到新对象s,用c去构造s,调用拷贝构造函数A(A),输出30。调用拷贝构造函数不是把c本身传给s,把c的值拷贝了一份给s去进行初始化。再将要退出主函数的时候,要将c对象,b对象,a对象释放掉,依次析构c,b,a,~A 30 ,~A 5 , ~A 5
fn函数的参数是值类类型,形参在调用这个函数的时候才给形参开辟空间,没有调用函数之前形参是没有的,所以s是新的。fn函数在即将退出的时候要将局部对象s析构,要调用析构函数,~A 30
调试结果:
在这里插入图片描述

3.函数返回值为类的对象,函数执行完成返回调用者时。
#include<iostream>
using namespace std;
class Point
{
public:
		Point(int xx = 0, int yy = 0):x(xx),y(yy)
	{

	}
	Point(Point& p);
	int getX()
	{
		return x;
	}
	int getY()
	{
		return y;
	}
private:
	int x, y;
};
Point::Point(Point& p):x(p.x),y(p.y)
{
	cout << "调用拷贝构造函数" << endl;
}
void f(Point p)
{
	cout << p.getX() << endl;
}
Point g()
{
	Point a(1, 2);
	return a;//函数的返回值是类的对象,返回函数值时,拷贝构造函数被调用
}
int main()
{
	Point a(1, 2);
	Point b(a);//用对象a初始化对象b,拷贝构造函数被调用
	cout << b.getX() << endl;
	Point c = a;//用对象a初始化对象c,拷贝构造函数被调用
	cout << c.getX() << endl;
	f(a);//函数的形参为类的对象,调用函数时,拷贝构造函数被调用
	Point d;
	d = g();
	cout << d.getX() << endl;
	return 0;
}

运行结果:
在这里插入图片描述
函数返回值时类类型的值返回时,由局部对象构造临时对象,局部对象是旧对象,临时对象是新对象,所以调用拷贝构造函数。
【例】

class A
{
public:
	A(int i = 0) :m_i(i)
	{
		cout << "A" << m_i << endl;
	}
	~A()
	{
		cout << "~A" << m_i << endl;
	}
	A(const A& t) :m_i(t.m_i)

	{
		cout << "A(A)" << m_i << endl;
	}

	void Print()
	{
		cout << m_i << endl;
	}
private:
	int m_i;
};
void fn(A s) 

{
	cout << "fn" << endl;

}
A test()
{
	A tt(60);//调用普通构造函数A(int),输出A60
	return tt;
	/*test函数在栈区,主函数也在栈区,当前不能从test函数返回到主函数中,两个栈区不能直接进行操作*/
	/*把tt这个局部变量先给了一个临时对象,临时对象是新的,
	从局部对象tt到临时对象的时候调用了一次拷贝构造函数。
	这时局部变量tt就消失了,由当前的临时对象把值带回来给了c,给了c之后,临时对象就可以消失了*/
}
void main()
{
	A a(5);//调用普通构造函数A(int),输出A5
	A b(a);//调用拷贝构造函数A(A),输出A5
	A c(30);//调用普通构造函数A(int),输出A30
	fn(c);//调用fn函数,调用拷贝构造函数
	c = test();//调用test函数,调用拷贝构造函数
	//注意:这句话中的c没有调用拷贝构造函数,这里的c调用的是赋值运算符重载
}

【分析】
test函数在栈区,主函数也在栈区,当前不能从test函数返回到主函数中,两个栈区不能直接进行操作。所以在调用test函数的时候,把tt这个局部变量先给了一个临时对象,局部变量tt是旧对象,临时对象是新对象,从局部对象tt到临时对象的时候调用了拷贝构造函数。这时局部变量tt就消失了,由当前的临时对象把值带回来给了c,当临时对象把值给了c之后,临时对象也就可以消失了。
调试结果:
在这里插入图片描述
结果分析:
在这里插入图片描述

总结:

调用拷贝构造函数的三种情况:
都是用旧对象去构造新对象
(1)用已有对象去初始化新对象;
(2)函数传参为类的对象的值传递时;
(3)函数返回值是类类型的值返回时。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐