关于拷贝构造函数与赋值构造函数的深刻解析
拷贝构造函数是C++最基础的概念之一,大家自认为对拷贝构造函数了解么?请大家先回答一下三个问题:
1. 以下函数哪个是拷贝构造函数,为什么?
X::X(const X&);
X::X(X);
X::X(X&, int a=1);
X::X(X&, int a=1, b=2);
2. 一个类中可以存在多于一个的拷贝构造函数吗?
3. 写出以下程序段的输出结果, 并说明为什么? 如果你都能回答无误的话,那么你已经对拷贝构造函数有了相当的了解。
#include <iostream>
using namespace std;
#include <string>
struct X
{
template <typename T> X(T &)
{
cout<<"This is a ctor."<<endl;
}
template <typename T> X & operator = (T &)
{
cout<<"This is a ctor2."<<endl;
}
};
void main()
{
X a(5);
X b(10.5);
X c=a;
c=b;
}
解答如下:
1. 对于一个类X,如果一个构造函数的第一个参数是下列之一:a) X&b) const X&c) volatile X&d) const volatile X&且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数.
1. X::X(const X&); //是拷贝构造函数
2. X::X(X&, int=1); //是拷贝构造函数
2.类中可以存在超过一个拷贝构造函数,
class X {
public:
X(const X&);
X(X&); // OK
};
注意,如果一个类中只存在一个参数为X&的拷贝构造函数,那么就不能使用const X或volatile X的对象实行拷贝初始化.
class X {
public:
X();
X(X&);
};
const X cx;
X x = cx; // error
如果一个类中没有定义拷贝构造函数,那么编译器会自动产生一个默认的拷贝构造函数.这个默认的参数可能为X::X(const X&)或X::X(X&),由编译器根据上下文决定选择哪一个.
默认拷贝构造函数的行为如下:
默认的拷贝构造函数执行的顺序与其他用户定义的构造函数相同,执行先父类后子类的构造.
拷贝构造函数对类中每一个数据成员执行成员拷贝(memberwise Copy)的动作.
a)如果数据成员为某一个类的实例,那么调用此类的拷贝构造函数.
b)如果数据成员是一个数组,对数组的每一个执行按位拷贝.
c)如果数据成员是一个数量,如int,double,那么调用系统内建的赋值运算符对其进行赋值.
深拷与浅拷
深拷贝和浅拷贝的定义可以简单理解成:如果一个类拥有资源(堆,或者是其它系统资源),当这个类的对象发生复制过程的时候(复制指针所指向的值),这个过程就可以叫做深拷贝,反之对象存在资源但复制过程并未复制资源(只复制了指针所指的地址)的情况视为浅拷贝。
浅拷贝资源后在释放资源的时候会产生资源归属不清的情况导致程序运行出错,这点尤其需要注意!
原则上,应该为所有包含动态分配成员的类都提供拷贝构造函数。
浅:
#include <iostream>
using namespace std;
#include <string>
class Ctor
{
public:
int *pointer;
public:
Ctor(int i=0)
{
pointer=new int(i);
}
~Ctor()
{
delete pointer;
}
void change(int i);
};
void Ctor::change(int i)
{
*pointer=i;
}
int main()
{
Ctor p1(2);
Ctor p2(p1);
p1.change(3);
cout<<*p2.pointer<<endl;
getchar();
return 0;
}
注意:对象p1值的改变引起了对象2值的改变
2 拷贝构造函数的另一种调用
当对象直接作为参数传给函数时,函数将建立对象的临时拷贝,这个拷贝过程也将调用拷贝构造函数。
例如:
#include <iostream>
using namespace std;
class Date
{
int n;
public:
Date(int i = 0)
{
cout << "载入构造函数" << endl;
n = i;
}
Date(const Date &d)
{
cout << "载入拷贝构造函数" << endl;
n = d.n;
}
int GetMember()
{
return n;
}
};
void Display(Date obj) //针对obj的操作实际上是针对复制后的临时拷贝进行的
{
cout << obj.GetMember() << endl;
}
int main()
{
Date a;
Date b(99);
Display(a); //对象直接作为参数
Display(b); //对象直接作为参数
getchar();
return 0;
}
还有一种情况,也是与临时对象有关的。
当函数中的局部对象被用作返回值,返回给函数调用时,也将建立此局部对象的一个临时拷贝,此时拷贝构造函数也将被调用。——可是经测试发现情况有异。
代码如下:
#include <iostream>
using namespace std;
class Date
{
int n;
public:
Date(int i = 0)
{
cout << "载入构造函数" << endl;
n = i;
}
Date(const Date &d)
{
cout << "载入拷贝构造函数" << endl;
n = d.n;
}
void Show()
{
cout << "n = " << n << endl;
}
};
Date GetClass(void) //函数中的局部对象被用作返回值,按理说应该引用拷贝构造函数
{
Date temp(100);
return temp;
}
int main()
{
Date a;
a.Show();
a = GetClass();//这里GetClass()函数中的局部对象被用作返回值
a.Show()
Date b = GetClass();//这里GetClass()函数中的局部对象被用作返回值
b.Show();
getchar();
return 0;
}
按理第2个和第3个应该输出'载入拷贝构造函数"才对,这个结果与预想的不一样,到底是哪里出问题了呢?
注:后来有论坛上的朋友告诉我说这是因为编译器的不同而导致不同的输出。
无名对象
现在我们来说一下无名对象。什么是无名对象?利用无名对象初始化对象系统不会调用拷贝构造函数?这是我们需要回答的两个问题。
首先我们来回答第一个问题。很简单,如果在程序的main函数中有:
Internet ("中国"); //Internet表示一个类
这样的一句语句就会产生一个无名对象。
无名对象会调用构造函数,但利用无名对象初始化对象时系统不会调用拷贝构造函数!
#include <iostream>
using namespace std;
class Date
{
int n;
public:
Date(int i = 0)
{
cout << "载入构造函数" << endl;
n = i;
}
Date(const Date &d)
{
cout << "载入拷贝构造函数" << endl;
n = d.n;
}
void Show()
{
cout << "n = " << n << endl;
}
};
int main()
{
Date a(100);
a.Show();
Date b = a; //"="在对象声明语句中,表示初始化,调用拷贝构造函数
b.Show();
Date c;
c.Show();
c = a; //"="在赋值语句中,表示赋值操作,调用赋值函数
c.Show();
getchar();
return 0;
}
赋值符的重载
由于并非所有的对象都会使用拷贝构造函数和赋值函数,程序员可能对这两个函数有些轻视。请先记住以下的警告,在阅读正文时就会多心:
本章开头讲过,如果不主动编写拷贝构造函数和赋值函数,编译器将以“位拷贝”的方式自动生成缺省的函数。倘若类中含有指针变量,那么这两个缺省的函数就隐含了错误。以类String的两个对象a,b为例,假设a.m_data的内容为“hello”,b.m_data的内容为“world”。
现将a赋给b,缺省赋值函数的“位拷贝”意味着执行b.m_data = a.m_data。这将造成三个错误:一是b.m_data原有的内存没被释放,造成内存泄露;二是b.m_data和a.m_data指向同一块内存,a或b任何一方变动都会影响另一方;三是在对象被析构时,m_data被释放了两次。
拷贝构造函数和赋值函数非常容易混淆,常导致错写、错用。拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用。以下程序中,第三个语句和第四个语句很相似,你分得清楚哪个调用了拷贝构造函数,哪个调用了赋值函数吗?
String a(“hello”);
String b(“world”);
String c = a; // 调用了拷贝构造函数,最好写成 c(a);
c = b; // 调用了赋值函数
本例中第三个语句的风格较差,宜改写成String c(a) 以区别于第四个语句。
请看下面的代码:
(1) 没有重载赋值函数
#include <iostream>
using namespace std;
class Product
{
public:
int* pointer;
Product(int i=0)
{
cout<<"调用构造函数"<<endl;
this->pointer=new int(i);
}
//change class variable
void change(int i)
{
*pointer=i;
}
// copying constructor
Product(const Product &p)//表示不允许在函数中改变p对象的值
{
this->pointer=new int(*p.pointer);
cout<<"调用拷贝构造函数"<<endl;
}
//deconstructor
~Product()
{
cout<<"调用析构函数"<<endl;
delete pointer;
}
};
int main()
{
Product p1(1);
Product p2(2);
Product p3(3);
p2=p3;
p3.change(4);
cout<<*p2.pointer<<endl;
return 0;
}
(2)重载赋值函数
#include <iostream>
using namespace std;
class Product
{
public:
int* pointer;
Product(int i=0)
{
cout<<"调用构造函数"<<endl;
this->pointer=new int(i);
}
//change class variable
void change(int i)
{
*pointer=i;
}
// copying constructor
Product(const Product &p)//表示不允许在函数中改变p对象的值
{
this->pointer=new int(*p.pointer);
cout<<"调用拷贝构造函数"<<endl;
}
//重载=运算符(赋值构造函数)
Product & operator=(const Product &p)
{
if(this!=&p)
this->pointer=new int(*p.pointer);
return *this;
}
//deconstructor
~Product()
{
cout<<"调用析构函数"<<endl;
delete pointer;
}
};
int main()
{
Product p1(1);
Product p2(2);
Product p3(3);
p2=p3;
p3.change(4);
cout<<*p2.pointer<<endl;
return 0;
}
在拷贝构造函数中使用赋值函数
为了简化程序,我们通常在拷贝构造函数中使用赋值函数。
例如:
#include <iostream>
using namespace std;
class Date
{
int da, mo, yr;
public:
Date(int d = 0, int m = 0, int y = 0)
{
cout << "载入构造函数" << endl;
da = d;
mo = m;
yr = y;
}
Date(const Date &other);
Date & operator =(const Date &other);
void Show()
{
cout << mo << "-" << da << "-" << yr << endl;
}
};
Date::Date(const Date &other)
{
cout << "载入拷贝构造函数" << endl;
*this = other;//拷贝构造函数中使用赋值函数
}
Date & Date::operator =(const Date &other)
{
cout << "载入赋值函数" << endl;
if(this == &other)
return *this;
da = other.da;
mo = other.mo;
yr = other.yr;
return *this;
}
int main()
{
Date a(1, 3, 6);
a.Show();
Date b = a;
b.Show();
Date c;
c.Show();
c = a;
c.Show();
return 0;
}
注意后面改写后的结果:拷贝构造函数与赋值构造函数
请注意:程序输出了两次“载入赋值函数”,这是因为我们在拷贝构造函数中使用了赋值函数,这样使程序变得简洁。如果把拷贝构造函数改写为:
Date::Date(const Date &other)
{
cout << "载入拷贝构造函数" << endl;
da = other.da;
mo = other.mo;
yr = other.yr;
}
则程序将输出:
载入构造函数:
3-1-6
载入拷贝构造函数
3-1-6
载入构造函数:
0-0-0
载入赋值函数
3-1-6
偷懒的办法处理拷贝构造函数和赋值函数
如果我们实在不想编写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数,怎么办?
偷懒的办法是:只需将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。
例如:
class A
{
private:
A(const A &a); // 私有的拷贝构造函数
A & operator =(const A &a); // 私有的赋值函数
};
如果有人试图编写如下程序:
A b(a); // 调用了私有的拷贝构造函数
b = a; // 调用了私有的赋值函数
编译器将指出错误,因为外界不可以操作A的私有函数。(引自〈〈高质量c++编程指南〉〉)