复制构造函数是一种特殊的构造函数,具有单个形参,该形参(常用const修饰)是对该类型的引用。
1.当定义一个新对象并用一个同类型的对象对它进行初始化时,将显示使用复制构造函数;
2.当将该类型的对象传递给函数或从函数返回该类型的对象时,将隐式使用复制构造函数。
当对象超出作用域或动态分配的对象被删除时,将自动应用析构函数。析构函数可用于释放对象构造时或在对象生命期中所获取的资源。不管类是否定义了自己的析构函数,编译器都自动执行类中非static数据成员的析构函数。
复制构造函数,赋值操作符和析构函数总称为复制控制。有一种特别常见的情况需要类定义自己的复制控制成员的:类具有指针成员。
13.1 复制构造函数
c++支持两种初始化形式:直接初始化和复制初始化。复制初始化使用"=",直接初始化将初始化式放在圆括号中。
string null_book="9-999-99999-9";//复制
string dot(10,'.');//直接
string empty_copy=string();//复制
string empty_direct;//直接
当形参为非引用类型的时候,将复制实参的值。类似地,以非引用类型做返回值时,将返回return语句中的值的副本。当形参或返回值为类类型时,由复制构造函数进行复制。
即使定义了其他构造函数,也会合成复制构造函数。合成复制构造函数的行为是,执行“逐个成员”初始化,将新对象初始化为原对象的副本。
“逐个成员”是指编译器将现有对象的每个非static成员,依次复制到正在创建的对象。合成复制构造函数直接复制内置类型成员的值,类类型成员使用该类的复制构造函数进行复制。数组成员的复制是一个例外。虽然一般不能复制数组,但如果一个类加油数组成员,则合成复制构造函数将复制数组。复制数组时合成复制构造函数将复制数组的每一个元素。
复制构造函数就是接受单个类类型引用形参(通常为const)的构造函数,如:
class Foo{
Foo();//构造函数
Foo(Const Foo &);//复制构造函数
};
有些类必须对复制对象时发生的事情加以控制。这样的类经常有一个数据成员是指针,或者有成员表示在构造函数中分配的其他资源。而另一些类在创建新对象时必须做一些特定工作。在这两种情况下,都必须定义复制构造函数。
复制构造函数的形参并不限制为const,但必须是一个引用。我自己的理解是:“如果不是引用,形参需调用拷贝构造函数来复制实参,这种调用是不合理的。”
Sales_item::Sales_item(Const Sales_item rhs);//error
为了防止复制,类必须显示声明其复制构造函数为private。
如果复制构造函数是private,将不允许用户代码复制该类类型的对象,编译器将拒绝任何进行复制的尝试。然而类的友元和成员仍可进行复制。如果想要连友元和成员中的复制也禁止,就可以声明一个(private)构造函数但不对其定义。
一般来说,最好显示或隐式定义默认构造函数和复制函数。只有不存在其他构造函数时才合成默认构造函数。如果定义了复制构造函数,也必须定义默认构造函数。
13.2赋值操作符
class Sales_item{
public:
Sales_item& operator=(const Sales_item&);//赋值操作符
};
合成赋值操作符与合成复制构造函数的操作类似。它逐个成员赋值:右操作数的每个成员赋值给左操作数对象的对应成员。除数组之外,每个成员用所属类型的常规方式赋值。对数组,每个数组元素赋值。
Sales_item& Sales_item::operator=(const Sales_item &rhs)
{
isbn=rhs.isbn;
units_sold=rhs.units_sold;
revenue=rhs.revenue;
return *this;
}
13.3析构函数
撤销类对象时会自动调用析构函数:
Sales_item *p=new Sales_item;//调用默认构造函数
{
Sales_item item(*p);//调用拷贝构造函数复制*p到item
delete p;//调用析构函数析构*p
}//变量item在超出作业域时应该自动撤销。因此,当遇到右花括号时,将运行item的析构函数。
动态分配的对象只有在指向该对象的指针被删除时才撤销。如果没有删除指向动态对象的指针,则不会与运行该对象的析构函数,对象就一直存在,从而导致内存泄露,而且,对象内部使用的如何资源也不会释放。
当对象的引用或指针超出作用域时,不会运行析构函数。只有删除指向动态分配对象的指针或实际对象(而不是引用)超出作用域时,才会运行析构函数。
撤销一个容器(标准库容器或内置数组)时,都会运行容器中类类型元素的析构函数。容器中的元素总是按逆序撤销:首先撤销下标为size()-1的元素,然后是size()-2的元素......直至最后撤销下标为0的元素。
#include<iostream>
using namespace std;
class A{
public:
A();
~A();
private:
static int data;
};
int A::data=0;
A::A(){
data=data+1;
cout<<"constructed "<<data<<endl;
}
A::~A(){
data=data-1;
cout<<"destructed "<<data<<endl;
}
int main(){
A *a=new A[10];
delete []a;//如果不显示的delete,并不会调用析构函数来释放动态分配的对象
return 0;
}
一般如果类需要析构函数,则它需要赋值操作符和复制构造函数,这是一个有用的经验法则。
合成析构函数按对象创建时的逆序撤销每个非static成员。
析构函数是个成员函数,它的名字是在类名字之前加上“~”,它没有返回值,没有形参,所以不能重载析构函数。析构函数与复制构造函数或赋值操作符之间的一个重要区别是,即使编写了自己的析构函数,合成析构函数仍然运行。
13.5 管理指针成员
包含指针的类需要特别注意复制控制,原因是复制指针时只需复制指针中的地址,而不会复制指针指向的对象。
大多数C++采用以下三种方法之一管理指针成员:
1.指针成员采取常规指针型行为。这样的类具有指针所有的缺陷但无需特殊的复制控制。
2.类可以实现所谓的“智能指针”行为。指针所指向的对象是共享的,但类能够防止“悬垂指针”。
3.类采取值型行为。指针所指向的对象是唯一的,由每个类对象独立管理。
#include<iostream>
using namespace std;
class HasPtr{
public:
HasPtr(int *p,int i):ptr(p),val(i){}
int *get_ptr()const {return ptr;}
int get_int()const {return val;}
void set_ptr(int *p){ptr=p;}
void set_int(int i){val=i;}
int get_ptr_val()const {return *ptr;}
void set_ptr_val(int val)const {*ptr=val;}
private:
int *ptr;
int val;
};
int main()
{
int obj=0;
HasPtr ptr1(&obj,42);
cout<<ptr1.get_ptr_val()<<'\t'<<ptr1.get_int()<<endl;//0 42
HasPtr ptr2(ptr1);//复制之后,int的值是独立的,而指针的值却纠缠在一起
cout<<ptr2.get_ptr_val()<<'\t'<<ptr2.get_int()<<endl;//0 42
ptr1.set_int(0);
cout<<ptr1.get_int()<<endl;//0
cout<<ptr2.get_int()<<endl;//42
ptr1.set_ptr_val(42);
cout<<ptr2.get_ptr_val()<<endl;//42
return 0;
}
可能出现悬垂指针。
int *ip=new int(42);
HasPtr ptr(ip,10);
delete ip;
ptr.set_ptr_val(0);
这里的问题是ip和ptr中的指针指向同一对象。删除了该对象时,ptr中的指针不再指向有效对象。然而,没有办法得知对象已经不存在了。
定义智能指针的通用技术是采用一个使用计数。智能指针类将一个计算器与类指向的对象相关联。使用计数跟踪该类有多少个对象共享同一指针。使用计数为0时,删除对象。
每次创建类的新对象时,初始化指针并将使用计数置为1。当对象作为另一对象的副本而创建时,复制构造函数复制指针并增加与之相应的使用计数的值。对一个对象进行赋值时,赋值操作符减少左操作数所指对象的使用计数的值(如果使用计数减至0,则删除对象),并增加右操作数所指对象的使用计数的值。最后调用析构函数时,析构函数减少使用计数的值,如果计数减至0,则删除基础对象。
使用计数的一种策略是:需要定义一个单独的具体类用以封装使用计数和相关指针:
class U_Ptr{
friend class HasPtr;
int *ip;
size_t use;
U_Ptr(int *p):ip(p),use(1){}
~U_Ptr(){delete ip;}
};
class HasPtr{
public:
HasPtr(int *p,int i):ptr(new U_Ptr(p)),val(i){}
HasPtr(const HasPtr &orig):ptr(orig.ptr),val(orig.val){++ptr->use;}
HasPtr& operator= (const HasPtr&);
~HasPtr(){if(--ptr->usr==0) delete ptr;}
private:
U_Ptr *ptr;
int val;
};
HasPtr& HasPre::operator=(const HasPtr &rhs)
{
++rhs->use;
if(--ptr->use==0)
delete ptr;
ptr=rhs.ptr;
val=rhs.val;
return *this;
}
为了管理具有指针成员的类,必须定义三个复制控制成员:复制构造函数,赋值操作符,析构函数。这些成员可以定义指针成员的指针型行为或值型行为。
值型类将指针成员所指的基础值的副本给每个对象。复制构造函数分配新元素并从被复制对象处复制值,赋值操作符撤销所保存的原对象并从右操作数向左操作数复制值,析构函数撤销对象。
“智能指针”的类在对象间共享同一基础值,从而提供指针行为。为了实现智能指针的行为,类需要保证基础对象一直存在,直到最后一个副本消失。复制构造函数将指针从旧对象复制到新对象时,会将使用计数加1。赋值操作符将左操作数的使用计数减1并将右操作数的使用计数加1,如果左操作数的使用计数减为0,赋值操作符必须删除它所指向的对象,最后,赋值操作符将指针从右操作数复制到左操作数。析构函数将使用计数减1,并且,如果使用计数减至0,就删除基础对象。
class HasPtr{
public:
HasPtr(const int &p,int i):ptr(new int(p)),val(i){}
HasPtr(const HasPtr &orig):ptr(new int(*orig.ptr)),val(orig.val){}
HasPtr& operator=(const HasPtr&);
~HasPtr(){delete ptr;}
int get_ptr_val()const {return *ptr;}
int get_int()const {return val;}
void set_ptr(int *p){ptr=p;}
void set_int(int i){val=i;}
int *get_ptr()const {return ptr;}
void set_ptr_val(int p)const { *ptr=p;}
private:
int *ptr;
int val;
};
HasPtr& operator=(const HasPtr& rhs)
{
*ptr=*rhs.ptr;
val=rhs.val;
return *this;
}