C++之复制控制
只有单个形参,而且该形参是对本类类型对象的引用(常用const修饰),这样的构造函数叫做复制构造函数(有时也称为拷贝构造函数),例如:
class Person{
public:
Person();//默认构造函数
Person(const Person&);//复制构造函数
....................
};
与默认构造函数一样,复制构造函数可由编译器隐式调用,它主要用于以下几种情况:
(1)根据另一个同类型的对象显示或者隐式初始化一个对象
(2)复制一个对象,将它作为实参传给一个函数
(3)从函数返回时复制一个对象
(4)初始化顺序容器中的对象
(5)根据元素初始化式列表初始化数组元素
C++支持两种初始化形式:直接初始化和复制初始化,复制初始化使用=符号,而直接初始化将初始化式放在圆括号中,直接初始化直接调用与实参匹配的构造函数,复制初始化总是调用复制构造函数。复制初始化首先使用指定构造函数创建一个临时的对象,然后用复制构造函数将那个临时的对象复制到正在创建的对象。例如:
string book="9-999-99999-100";
上式创建book时,编译器首先调用接受一个C风格字符串形参的string构造函数,创建一个临时的对象,然后编译器使用string复制构造函数将book初始化为那个临时对象的副本。
当形参为非引用类型的时候,将复制实参的值,类似的,以非引用类型作返回值时,将返回return语句中的值的副本,因此,当形参或者返回值为类类型时,将由复制构造函数进行复制;复制构造函数可以用于初始化顺序容器中的元素,例如:
vector<string> svec(5);
容器的这种构造方式使用了默认构造函数和复制构造函数。编译器首先使用string默认构造函数创建一个临时值svec,然后使用复制构造函数将临时值复制到svec的每个元素;如果没有为类类型数组提供元素初始化式,则将用默认构造函数初始化每个元素,不过,如果使用常规的花括号括住的数组初始化列表来提供显式元素初始化式,则使用复制初始化开初始化每个元素。编译器根据指定值创建适当类型的元素,然后用复制构造函数将该值复制到相应元素:
string book[]={ string("0-201-201"),
string("0-201-202"),
string("0-201-203")
};
如果我们没有定义复制构造函数,编译器就会为我们合成一个,不过,与合成的默认构造函数不同,即使我们定义了其他构造函数,也会合成复制构造函数。合成复制构造函数将执行逐个成员初始化,完成对象之间的位拷贝(位拷贝又称浅拷贝),将新对象初始化为原对象的副本。所谓的“逐个成员”在,指的是编译器将现有对象的每个非static成员,依次复制到正在创建的对象。合成复制构造函数直接复制内置类型成员的值,类类型成员使用该类的复制构造函数进行复制。数组成员的复制是个例外,虽然一般不能复制数组,但如果一个类具有数组成员,则合成复制构造函数将复制数组,而且合成复制构造函数将复制数组的没一个元素。
对于许多类来讲,合成复制构造函数只完成必要的工作,只包含类类型成员或内置类型(但不是指针类型)成员的类,无须显示定义复制构造函数,也可以复制,这种情况叫做“浅拷贝”。不过,有些类必须对复制对象时发生的事情加以控制,这样的类经常有一个数据成员是指针,或者有成员表示在构造函数中分配的其他资源,而也有一些类在创建新对象时必须做一些特定工作,这些情况下,都必须定义复制构造函数,这种情况下叫“深拷贝”。
对于普通类型的对象来说,它们之间的复制是很简单的,例如:
int a=100;
int b=a;
而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量。下面看一个类对象拷贝的简单例子。
#include <iostream>
using namespace std;
class Test
{
public:
Test(int b)
{
a=b;
}
void Show ()
{
cout<<a<<endl;
}
private:
int a;
};
int main()
{
Test A(100);
Test B=A;
B.Show ();
return 0;
}
运行程序,屏幕输出100。从以上代码的运行结果可以看出,系统为对象B分配了内存并完成了与对象A的复制过程。就类对象而言,相同类型的类对象是通过拷贝构造函数来完成整个复制过程的。
浅拷贝和深拷贝
在某些状况下,类内成员变量需要动态开辟堆内存,假如实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,假如B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。
深拷贝和浅拷贝可以简单理解为:假如一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。下面举个深拷贝的例子。
#include <iostream>
using namespace std;
class Test
{
public:
Test(int b,char* cstr)
{
a=b;
str=new char[b];
strcpy(str,cstr);
}
Test(const Test& C)
{
a=C.a;
str=new char[a]; //深拷贝
if(str!=0)
strcpy(str,C.str);
}
void Show()
{
cout<<str<<endl;
}
~Test()
{
delete str;
}
private:
int a;
char *str;
};
int main()
{
Test A(10,"Hello!");
Test B=A;
B.Show();
return 0;
}
因此,复制构造函数也叫做拷贝构造函数,它分为浅拷贝,一般情况下,浅拷贝就合成复制构造函数可以完成,另外一种是深拷贝,也就是需要我们显示定义复制构造函数的一种情况了。
如果有些类需要完全禁止复制,那么只需将复制构造函数声明为私有成员函数即可,否则如果不声明定义,编译器将会默认合成一个复制构造函数,照样可以完成浅拷贝的复制构造过程,如:
#include <iostream>
using namespace std;
class Obj
{
public:
Obj()
{
cout << "默认构造函数" << endl;
}
private:
Obj(const Obj &obj)
{
cout << "复制构造函数" << endl;
}
};
int main()
{
Obj obj1;
Obj obj2(obj1);
return 0;
}
编译报错:
error: 'CObj::CObj(const CObj&)' is private
这就是禁止复制的效果,正是这里想要的。
如果想要连友元函数和成员函数的复制也禁止,那么可以声明一个私有的复制构造函数但不对其定义。因为友元函数或成员函数可以访问到类的私有成员,所以当然能调用私有的复制构造函数,所有将复制构造函数声明为私有但不定义,就能避免友元或成员函数的调用。声明而不定义成员函数是合法的,但是,使用未定义成员的任何尝试将导致链接失败。用户代码中的复制尝试将在编译时标记为错误,而成员函数和友元中的复制尝试将在链接时导致错误。
#include <iostream>
using namespace std;
class Obj
{
public:
Obj()
{
cout << "默认构造函数" << endl;
}
friend void fri_copy();
private:
Obj(const Obj &obj);
};
void fri_copy()()
{
CObj obj1;
CObj obj2(obj1);
}
int main()
{
fri_copy();
return 0;
}
编译报错:
undefined reference to `CObj::CObj(CObj const&)'
这也正是这里需要的结果,禁止复制成功。