C++拷贝控制详解
1、拷贝,赋值与销毁
1.1拷贝构造函数
拷贝构造函数:一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
class Foo {
public:
Foo() ;//默认构造函数
Foo(const Foo&); // 拷贝构造函数
//...
};
拷贝构造通常情况下都会被隐式地使用,因此不应该是explicit
1.1.1 合成拷贝构造函数
特点:
①编译器默认生成,即使我们定义了其他的构造函数
②功能:一般情况下是将每个非static成员拷贝到正在创建的对象中。
class Sales data {
public:
//其他成员和构造函数的定义,如前
//与合成的拷贝构造函数等价的拷贝构造函数的声明
Sales data (const Sales data&) ;
private:
std: :string bookNo;
int units sold = 0;
double revenue = 0.0;
};
//与Salesdata的合成的拷贝构造函数等价
Sales data: :Sales data (const Sales data &orig) :
bookNo (orig. bookNo),// 使用string的拷贝构造函数
units_ sold(orig.units_ sold) ,//拷贝orig.units sold :
revenue (orig. revenue) //拷贝orig. revenue
{ }
//空函数体
对于上面采用的构造函数的初始化列表,其中就使用了一些类的拷贝构造函数。
1.1.2拷贝初始化
string dots(10,' .') ; //直接初始化
string s (dots) ; //直接初始化
string s2 = dots; //拷贝初始化
string null_ book = "9-999-99999-9"; //拷贝初始化
string nines = string(100, ' 9') ; //拷贝初始化
拷贝初始化的发生情况:
- 使用=号时
- 将一个对象作为实参传递给一个非引用类型的形参
- 用花括号列表初始化一个数组中的元素或者是一个聚合类中的成员
另外,在我们使用标准库的容器的时候,调用其insert 或者push成员,容器会对其元素进行拷贝初始化。
1.1.3参数和返回值
函数调用过程中,非引用类型的参数要进行拷贝初始化。
函数具有非引用的返回类型,返回值会被用来初始化调用方的结果。
1.2 拷贝赋值运算符
Sales_data trans, accum;
trans = accum;
与拷贝构造函数相同,如果没写显式的拷贝赋值运算符,那么编译器会自己合成一个。
1.2.1重载赋值运算符
重载运算符的本质是函数,名字是operator加上要定义的运算符符号。所以,赋值运算符就是一个名为operator=的函数。也有其返回值和参数列表:
class Foo{
public:
Foo& operator=(const Foo&); //赋值运算符
//...
}
赋值运算符通常返回一个指向其左侧运算对象的引用。
1.2.2合成拷贝构造函数
1.3 析构函数
析构函数:释放对象使用的资源,销毁对象的非static 数据成员
class Foo{
public:
~Foo();
//...
}
析构函数按照初始化的顺序的逆序进行销毁。析构函数释放对象在生存期分配的所有资源。
析构部分是隐式的,成员析构时发生什么完全依赖于成员的类型。
隐式销毁一个内置的指针类型成员不会delete它所指向的对象。
1.3.1析构函数的调用时刻
无论何时一个对象被销毁,就会自动调用其析构函数:
- 变量在离开其作用域时被销毁。
- 当一个对象被销毁时,其成员被销毁。
- 容器(无论是标准库容器还是数组)被销毁时,其元素被销毁。
- 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁
- 对于临时对象,当创建它的完整表达式结束时被销毁。
对于一个shared_ptr,销毁时会递减其引用计数;如果引用计数变为0,对象被释放。
1.3.2合成析构函数
合成析构函数:一个类未定义自己的析构函数时,编译器为它定义一个合成析构函数。
一般情况下这个合成析构函数函数体为空,特别的,某些类的合成析构函数被用来组织该类型的对象被销毁。
1.4 三/五法则
拷贝构造函数,拷贝赋值运算符,析构函数,这三个函数通常联合使用,很少存在只使用部分的操作的情况。
1.4.1需要析构函数的类也通常需要拷贝和赋值操作
下面定义一个HasPtr类,假如如下只是定义析构函数而使用合成拷贝构造函数和合成的拷贝赋值运算符,那么有可能造成多个指针指向同一块内存,从而进行下面操作会对同一块内存销毁两次,这显然是不合理的。
class HasPtr {
public:
HasPtr (const std: :string &S = std: :string()) :
ps (new std: :string(s)),i(0) { }
~HasPtr() { delete ps; }
}
//错误: HasPtr需要一个拷贝构造函数和一个拷贝赋值运算符
//其他成员的定义,如前
};
HasPtr f (HasPtr hp){// HasPtr 是传值参数,所以将被拷贝
HasPtr ret = hp; //拷贝给定的HasPtr
//处理ret
return ret;//ret和hp被销毁
}
1.4.2需要拷贝构造函数的类也需要赋值操作,反之亦然
这个例子引出了第二个基本原则:如果一个类需要一个拷贝构造函数,几乎可以肯定它也需要一个拷贝赋值运算符。反之亦然——如果 一个类需要一个拷贝赋值运算符,几乎可以肯定它也需要一个拷贝构造函数。然而,无论是需要拷贝构造函数还是需要拷贝赋值运算符都不必然意味着也需要析构函数。
1.4.2需要拷贝构造函数的类也需要赋值操作,反之亦然
这个例子引出了第二个基本原则:如果一个类需要一个拷贝构造函数,几乎可以肯定它也需要一个拷贝赋值运算符。反之亦然——如果 一个类需要一个拷贝赋值运算符,几乎可以肯定它也需要一个拷贝构造函数。然而,无论是需要拷贝构造函数还是需要拷贝赋值运算符都不必然意味着也需要析构函数。