默认拷贝构造函数的行为如下: 默认的拷贝构造函数执行的顺序与其他用户定义的构造函数相同,执行先父类后子类的构造. 拷贝构造函数对类中每一个数据成员执行成员拷贝(memberwise Copy)的动作. a)如果数据成员为某一个类的实例,那么调用此类的拷贝构造函数. b)如果数据成员是一个数组,对数组的每一个执行按位拷贝. c)如果数据成员是一个数量,如int,double,那么调用系统内建的赋值运算符对其进行赋值.
1.深拷与浅拷
深拷贝和浅拷贝的定义可以简单理解成:如果一个类拥有资源(堆,或者是其它系统资源),当这个类的对象发生复制过程的时候(复制指针所指向的值),这个过程就可以叫做深拷贝,反之对象存在资源但复制过程并未复制资源(只复制了指针所指的地址)的情况视为浅拷贝。 浅拷贝资源后在释放资源的时候会产生资源归属不清的情况导致程序运行出错,这点尤其需要注意! 原则上,应该为所有包含动态分配成员的类都提供拷贝构造函数。
浅:
using namespace std;

//shallow && deep copy
//deep copy make pointer point to a new place!
class Product
 ...{
public:int* pointer;
Product(int i=0)
 ...{
pointer=new int(i);
}
//change class variable
void change(int i)
 ...{
*pointer=i;
}

//deconstructor
~Product()
 ...{
delete pointer;
}
};
int main()
 ...{
Product p1(2);
Product p2(p1);
p1.change(3);
cout<<*p2.pointer<<endl;

getchar();
return 0;
}
深:
using namespace std;

//shallow && deep copy
//deep copy make pointer point to a new place!
class Product
 ...{
public:int* pointer;
Product(int i=0)
 ...{
pointer=new int(i);
}
//change class variable
void change(int i)
 ...{
*pointer=i;
}

// copying constructor
Product(const Product &p)
 ...{
pointer=new int(*p.pointer);
}
//deconstructor
~Product()
 ...{
delete pointer;
}
};
int main()
 ...{
Product p1(2);
Product p2(p1);
p1.change(3);
cout<<*p2.pointer<<endl;

getchar();
return 0;
}
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;
}


程序输出: 载入构造函数: 载入构造函数: 载入拷贝构造函数 0载入拷贝构造函数 99 还有一种情况,也是与临时对象有关的。 当函数中的局部对象被用作返回值,返回给函数调用时,也将建立此局部对象的一个临时拷贝,此时拷贝构造函数也将被调用。——可是经测试发现情况有异。 代码如下:
#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;
}


程序输出: 载入构造函数: n = 0 载入构造函数: n = 100 载入构造函数: n = 100 按理第2个和第3个应该输出'载入拷贝构造函数"才对,这个结果与预想的不一样,到底是哪里出问题了呢? 注:后来有论坛上的朋友告诉我说这是因为编译器的不同而导致不同的输出。 有人得到这样的输出结果: 载入构造函数 n = 0 载入构造函数 载入拷贝构造函数 n = 100 载入构造函数 载入拷贝构造函数 n = 100 还有人得到这样的输出结果: 载入构造函数 n = 0 载入构造函数 载入拷贝构造函数 n = 100 载入构造函数 载入拷贝构造函数 载入拷贝构造函数 n = 100 (用的是vc++)
3.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;
}


程序输出: 载入构造函数: name的地址: 23ff40;name的字符串: 中国 cname的地址: 33778;cname的字符串: 中国 载入析构函数:
上面代码的运行结果有点“出人意料”,从思维逻辑上说,当无名对象创建了后,是应该调用自定义拷贝构造函数,或者是默认拷贝构造函数来完成复制过程的,但事实上系统并没有这么做,因为无名对象使用过后在整个程序中就失去了作用。对于这种情况c++会把代码看成是: Internet a ("中国"); 省略了创建无名对象这一过程,所以说不会调用拷贝构造函数。
3.赋值符的重载 由于并非所有的对象都会使用拷贝构造函数和赋值函数,程序员可能对这两个函数有些轻视。请先记住以下的警告,在阅读正文时就会多心: 本章开头讲过,如果不主动编写拷贝构造函数和赋值函数,编译器将以“位拷贝”的方式自动生成缺省的函数。倘若类中含有指针变量,那么这两个缺省的函数就隐含了错误。以类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 "stdafx.h"

using namespace std;

//shallow && deep copy
//deep copy make pointer point to a new place!
class Product
 ...{
public:int* pointer;
Product(int i=0)
 ...{
pointer=new int(i);
}
//change class variable
void change(int i)
 ...{
*pointer=i;
}

// copying constructor
Product(const Product &p)
 ...{
pointer=new int(*p.pointer);
}
//deconstructor
~Product()
 ...{
delete pointer;
}
};
int main()
 ...{
Product p1(1);
Product p2(2);
Product p3(3);
p2=p3;
p3.change(4);
cout<<*p2.pointer<<endl;

getchar();
return 0;
}
//结果输出4
(2)重载赋值函数
#include "stdafx.h"

using namespace std;

//shallow && deep copy
//deep copy make pointer point to a new place!
class Product
 ...{
public:int* pointer;
Product(int i=0)
 ...{
pointer=new int(i);
}
//change class variable
void change(int i)
 ...{
*pointer=i;
}

// copying constructor
Product(const Product &p)
 ...{
pointer=new int(*p.pointer);
}
//重载=
Product& operator=(const Product &p)
 ...{
if(this!=&p)
pointer=new int(*p.pointer);
return *this;
}
//deconstructor
~Product()
 ...{
delete pointer;
}
};
int main()
 ...{
Product p1(1);
Product p2(2);
Product p3(3);
p2=p3;
p3.change(4);
cout<<*p2.pointer<<endl;

getchar();
return 0;
}
//输出3
5. 在拷贝构造函数中使用赋值函数 为了简化程序,我们通常在拷贝构造函数中使用赋值函数。 例如:
#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();
getchar();
return 0;
}


程序输出: 载入构造函数: 3-1-6 载入拷贝构造函数 载入赋值函数 3-1-6 载入构造函数: 0-0-0 载入赋值函数 3-1-6 请注意:程序输出了两次“载入赋值函数”,这是因为我们在拷贝构造函数中使用了赋值函数,这样使程序变得简洁。如果把拷贝构造函数改写为: 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
6. 偷懒的办法处理拷贝构造函数和赋值函数 如果我们实在不想编写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数,怎么办? 偷懒的办法是:只需将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。 例如: class A { … private: A(const A &a); // 私有的拷贝构造函数 A & operator =(const A &a); // 私有的赋值函数 }; 如果有人试图编写如下程序: A b(a); // 调用了私有的拷贝构造函数 b = a; // 调用了私有的赋值函数 编译器将指出错误,因为外界不可以操作A的私有函数。(引自〈〈高质量c++编程指南〉〉)
|