拷贝构造函数是一种特殊的构造函数,具有一般构造函数的所有特性,其形参是本类的对象的引用。其作用是使用一个已经存在的对象(由拷贝构造函数的参数指定),去初始化同类的一个新对象。
如果程序员没有定义类的拷贝构造函数,系统会在必要时自动生成一个隐含的拷贝构造函数。这个隐含的拷贝构造函数的功能是:把初始值对象的每个数据成员的值都拷贝到新建立的对象中。这样就完成了同类对象的拷贝,这样得到的对象和原对象具有完全相同的数据成员,即完全相同的属性。
声明和实现拷贝构造函数的一般方法:
class 类名
{
public:
类名(形参表);//构造函数
类名(类名&对象名);//拷贝构造函数
...
};
类名::类名(类名&对象名)//拷贝构造函数的实现
{
函数体
}
【例】
通过水平和垂直两个方向的坐标值X和Y类来确定屏幕上的一个点。点Point类的定义如下:
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;
}
类中声明了内联构造函数和拷贝构造函数。
拷贝构造函数的实现如下:
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)函数返回值是类类型的值返回时。