C++ Primer : 第十三章 : 拷贝控制之拷贝、赋值与销毁

拷贝构造函数

一个构造函数的第一个参数是自身类类型的引用,额外的参数(如果有)都有默认值,那么这个构造函数是拷贝构造函数。拷贝构造函数的第一个参数必须是一个引用类型。

合成的拷贝构造函数  
在我们没有定义自己的拷贝构造函数,编译器会为我们合成一个。但是对于有些类来说,合成拷贝构造函数用来阻止我们拷贝该类类型的对象。而一般情况,合成拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中,编译器依次将每个非static成员拷贝到正在创建的对象中。

每个成员的类型决定了它如何拷贝:对类类型的成员, 会使用其拷贝构造函数来拷贝;内置类型的成员直接拷贝;对于数组而言不能直接拷贝,但是我们的拷贝构造函数会逐元素的拷贝一个数组类型的成员。


例如,Sales_data类的合成拷贝构造函数等价于:

class Sales_data {
public:
Sales_data(const Sales_data&); private:
std::string bookNo;
int units_sold = 0;
double revenue = 0.0; }; Sales_data::Sales_data(const Sales_data& data) : bookNo(data.bookNo),
units_sold(data.units_sold),
revenue(data.revenue)
{
}

拷贝初始化

string dots(10, '.'); // 直接初始化
string s(dots); // 直接初始化
string s2 = dots; // 拷贝初始化
string null_book = "9-999-99999-9"; // 拷贝初始化
string nines = string(100, '9'); // 拷贝初始化

直接初始化和拷贝初始化的区别:对于直接初始化,实际上时要求编译器调用普通的函数匹配来选择我们所提供的参数最匹配的构造函数。 对于拷贝初始化,要求编译器将右侧正在运算的对象拷贝到正常创建的对象中,需要的话进行类型转换。


拷贝初始化,除了发生在用 = 定义变量时发生,以下情况也会发生:
  • 将一个对象作为参数传递给一个非引用类型的形参
  • 将一个对象作为返回值从一个返回值为非引用类型的函数返回时
  • 用花括号列表初始化一个数组中的元素或者一个聚合类中的成员
除此之外,我们在调用标准库容器的insert成员或push成员时,容器会对其参数进行拷贝初始化;在调用emplace成员时,会调用与这个容器的构造函数参数最匹配的构造函数来进行直接初始化。


/* 非常重要 !!!*/
☆ 拷贝构造函数被用来初始化非引用类类型参数。 如果其参数不是引用类型,则调用永远不会成功,为了调用拷贝构造用函数,我们必须拷贝它的实参,但为 了拷贝实参,我们必须调用拷贝构造函数,如此无限循环。 

拷贝初始化也有限制,如果对一个类的初始化要求通过一个explicit的构造函数来进行类型转换,那么使用拷贝初始化和直接初始化就不是无关紧要的了:

vector<int> v1(10); // 正确,直接初始化
vector<int> v2 = 10; // 错误,接收大小参数的构造函数时explicit的
void f(vector<int>); // f的参数进行拷贝初始化
f(10); // 错误,不能用一个explicit的构造函数拷贝一个实参
f(vector<int>(10)); // 正确,从一个int直接构造一个临时vector

接受一个大小参数的构造函数时explicit的,意味着我们只能进行直接初始化而不能进行拷贝初始化;因此,我们不能用一个explicit的构造函数来拷贝一个参数,这里的拷贝有参数传递和函数的非引用返回值,如果我们希望使用一个explicit的构造函数时,必须显示的使用,比如上述代码的最后一行那样。



编译器可以绕过拷贝构造函数,在拷贝初始化的过程中,编译器可以 ( 但不是必须! ) 跳过拷贝/移动构造函数,直接创建对象:
string null_book = "123456";

可以改写为:

string null_book("123456");

即使有时候编译器可以跳过拷贝/移动构造函数,但是我们必须保证拷贝/移动构造函数是存在而且是可访问的(例如:不能是private)。



习题 13.13 (比较适合本节内容):
class MyClass{

public:

	MyClass(int data) : m_data(data){ std::cout << "Construct a MyClass! The m_data = " << m_data << std::endl;}

	MyClass(const MyClass& myclass){ this->m_data = myclass.m_data; std::cout << "Copy Construct a MyClass that right m_data = " << myclass.m_data << std::endl; }

	MyClass& operator = (const MyClass& myclass){

		this->m_data = myclass.m_data;
std::cout << "copy a MyClass that right m_data = " << myclass.m_data << std::endl;
return *this; } ~MyClass(){ std::cout << "Delete a MyClass that m_data = " << m_data << std::endl; } int getData(){return m_data;}
private: int m_data; }; int main(int argc, char** argv)
{ std::vector<MyClass> vecClass;
MyClass mclass1(1);
MyClass mclass2(2); std::cout << vecClass.capacity() << std::endl; // emplace_back进行直接初始化
vecClass.emplace_back( 1); std::cout << vecClass.capacity() << std::endl; // 由于vecClass的空间不能容下mclass2, 因此先拷贝一份mclass2,然后再析构原来的mclass2
vecClass.push_back(mclass2); std::cout << vecClass.capacity() << std::endl; MyClass* pMyClass = new MyClass(3); // 和上面同理
vecClass.insert(vecClass.cend(), *pMyClass); std::cout << vecClass.capacity() << std::endl; for (size_t i = 0; i < vecClass.size(); ++i)
std::cout << vecClass[i].getData() << std::endl; delete pMyClass; return 0;
}

输出为:

Construct a MyClass! The m_data = 1
Construct a MyClass! The m_data = 2
0
Construct a MyClass! The m_data = 1
1
Copy Construct a MyClass that right m_data = 1
Delete a MyClass that m_data = 1
Copy Construct a MyClass that right m_data = 2
2
Construct a MyClass! The m_data = 3
Copy Construct a MyClass that right m_data = 3
Copy Construct a MyClass that right m_data = 1
Copy Construct a MyClass that right m_data = 2
Delete a MyClass that m_data = 1
Delete a MyClass that m_data = 2
3
1
2
3
Delete a MyClass that m_data = 3
Delete a MyClass that m_data = 2
Delete a MyClass that m_data = 1
Delete a MyClass that m_data = 1
Delete a MyClass that m_data = 2
Delete a MyClass that m_data = 3

在调用emplace_back时,进行直接初始化,因此调用的是构造函数; 调用push_back时,进行拷贝初始化,调用拷贝构造函数,因为此时vecClass的空间不足,因此先拷贝了原来在vecClass中值为1的元素,然后销毁原来的vector,然后再拷贝新的元素,下面的代码中调用insert函数也是这样的道理,由于空间不够,先拷贝旧容器中的元素,然后销毁,再拷贝新的元素。





拷贝赋值运算符

如同类控制自己的拷贝初始化一样,类也可以控制其对象如何赋值:
Sales_data trans, accum;
trans = accum; // 使用Sales_data的拷贝赋值运算符

如果类未定义自己的拷贝赋值运算符,则编译器替我们合成一个。


拷贝赋值运算符是一个重载的赋值运算符
重载运算符本质上时函数,由operator关键字后跟要定义的运算符的符号组成。重载运算符的参数表示运算符的运算对象。
拷贝赋值运算符必须定义为成员函数,其左则对象被绑定到隐式的this指针上。
拷贝赋值运算符接受一个与其所在类相同类型的参数:
class Foo {

public:
Foo& operator =(const Foo&); // 拷贝赋值运算符 // ...
};

为了和内置类型的赋值保持一致,类的拷贝赋值运算符通常返回一个指向其左则对象的引用。应当注意的是,标准库要求保存的类型要具有赋值运算符,且返回值是左则运算对象的引用。


合成的拷贝赋值运算符

如果我们未对一个类定义自己的拷贝赋值运算符,那么编译器负责为我们生成一个合成的拷贝赋值运算符。它会将右侧的运算对象的每个非static成员赋予左侧对象的对应成员,这一操作时通过成员的赋值运算符来完成的。对于数组类型,逐个赋值数组元素。 合成的拷贝赋值运算符返回一个指向其左侧对象的引用。

Sales_data的合成拷贝赋值运算符的等价形式:
Sales_data& Sales_data::operator = (const Sales_data& data) {
bookNo = data.bookNo; // 调用std::string::operator =
units_sold = data.units_sold; // 使用内置的int赋值
revenue = data.revenue; // 使用内置的double赋值
return *this; // 返回左则运算对象的引用
}


析构函数

析构函数执行和构造函数相反的操作,构造函数用于初始化非statci成员,还可能做其他的一些工作,析构函数用于释放对象使用的资源,并销毁对象的非static成员。
class Foo {

public:
// 其他操作
~Foo(); // 析构函数
};

析构函数由波浪号加类名组成,没有返回值,不接受参数列表。


析构函数不接受参数,因此它不能被重载。对于一个给定的类,它又唯一的一个析构函数。

在一个构造函数中,数据成员的初始化在函数体执行之前执行的,初始化的顺序和它们在类中出现的顺序一致 , 对于析构函数来说,数据成员的释放在函数体之后进行, 成员按照其初始化顺序的逆序进行销毁。



在析构函数中,不存在类似构造函数中初始化列表的东西来控制成员如何销毁。析构部分是隐式的,成员销毁在销毁时完全决定于成员的类型,如果是类类型,则执行类的析构函数,内置类型销毁什么也不需要做。 隐式销毁一个内置指针类型的成员不会delete它所指向的对象。

智能指针与普通指针不同,因为智能指针时类类型,因此智能指针有析构函数,智能指针成员在析构阶段会被自动销毁。

无论何时一个对象被销毁,就会自动调用析构函数:
变量在离开其作用域时被销毁。
  当一个对象被销毁时,其成员被销毁。
容器(无论是标准库容器还是数组)被销毁时,其元素被销毁。
对于动态分配的对象,当对指向它的指针delete时被销毁
对于临时对象,当创建它的完整表达式结束时被销毁。


{
Sales_data* p = new Sales_data; // p是一个内置指针
auto p2 = make_shared<Sales_data>(); // p2是一个shared_ptr Sales_data item(*p); // 拷贝构造函数将*p拷贝到item中
vector<Sales_data> vec;
vec.push_back(*p2); // 拷贝p2指向的对象,增加p2的计数
delete p; // 对p指向的对象执行析构函数
} // 退出局部作用域,对item、vec、p2调用析构函数
// 销毁p2会递减其引用计数,如果引用计数变为0, 对象被释放
// 销毁vec会销毁它的元素

当指向一个对象的引用或指针离开作用域时,析构函数不会执行。



合成的析构函数

类似拷贝构造函数和拷贝赋值运算符,如果类没有定义自己的析构函数,编译器为这个类生成一个合成的析构函数。 合成的析构函数的函数体是空的。
析构函数自身并不直接销毁成员,成员是在析构函数体之后隐含的析构阶段中被销毁的,在整个对象的销毁时,析构函数体是作为成员销毁步骤之外的另一部分而进行的, 例如对内置指针的delete操作。


三/五法则

需要析构函数的类也需要拷贝和赋值操作

当我们决定一个类是否需要它自己版本的拷贝控制成员时,一个基本的原则是这个类是否需要一个析构函数。如果一个类需要一个析构函数,几乎可以肯定它也需要一个拷贝构造函数和一个拷贝赋值运算符。

class HasPtr {

public:
HasPtr(const std::string& s = std::string()) : ps(new std::string(s)), i(0){} private:
std::string* ps;
int i;
};

对于HasPtr类来说,其构造函数为ps分配动态内存,合成的析构函数不会delete一个指针数据成员,因此,这个类需要定义自己的析构函数:

~HasPtr() { delete ps; }

参考上面的原则,我们理应给HasPtr定义一个拷贝构造函数和拷贝赋值运算符,如果我们使用合成的拷贝构造函数和拷贝赋值运算符时, 将会发生严重的错误。

我们在拷贝HasPtr对象时,这些函数简单的拷贝指针成员,意味着多个HasPtr对象可能指向相同的内存:
HasPtr f (HasPtr hp) {
HasPtr ret = hp; // 拷贝给定的HasPtr
return ret; // ret和hp被释放
}

当f返回时,hp和ret都被释放,两个对象的析构函数都被执行。于是会delete掉ret和hp的指针成员,但这个两个对象包含相同的指针值,因此会导致此指针被delete两次,发生了一个严重的错误; 另外使用函数 f 的返回值作为初始化值的对象时,指针被销毁,指向无效地址。因此,我们需要定义自己的拷贝构造函数和拷贝赋值运算符。


HasPtr& HasPtr(const HasPtr& has) {

	ps  = new std::string(*(has.ps));
i = has.i;
return *this;
}

需要拷贝操作的类也需要赋值操作,反之亦然


如果一个类需要一个拷贝构造函数,几乎可以肯定这个类也需要一个拷贝赋值运算符, 反之亦然。




=default和=delete

=default:
我们可以将拷贝控制成员定义为=default来显示的要求编译器生成合成的版本。
class Sales_data {

public:
Sales_data() = default;
Sales_data(const Sales_data&) = default;
Sales_data& operator =(const Sales_data&);
~Sales_data() = default;
// ...
}; Sales_data& Sales_data::operator = (const Sales_data& ) = default;

在类内使用=default修饰成员的声明时,合成的函数隐式的被声明为内联的,如果不希望是内联的,我们应该在类外定义使用=default。

只能对具有合成版本的成员函数使用=default。

=delete:

虽然大多数类应该定义拷贝构造函数和拷贝赋值运算符,但是对于某些类来说,这些操作没有合理的意义。例如,iostream类阻止了拷贝,一避免多个对象写入或读取相同的IO缓冲。

在新的标准下,我们可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝。在函数的参数列表后面加上=delete 指出我们希望将它定义为删除的。我们虽然声明了它们,但不能以任何方式使用它们。

struct NoCopy {
NoCopy() = default;
NoCopy(const NoCopy&) = delete; // 阻止拷贝
NoCopy& operator = (const NoCopy&) = delete; // 阻止赋值
~NoCopy() = default;
// 其他成员
};

=delete告诉编译器,我们不希望定义这些函数。


=delete必须出现在函数第一次声明的时候。而且我们可以对任何函数指定=delete限定符。这是=delete和=default不同的两点。

析构函数不能是删除的成员。如果析构函数被定义为删除的,那么这个对象就无法销毁了,因此这个类也就无法定义它的变量或成员,但是可以动态分配这个类的对象,但是不能delete这个对象。

前面说到,某些类的合成拷贝构造函数版本阻止拷贝对象,因为编译器合成的拷贝构造函数定义为删除的:


  • 如果类的某个成员的析构函数是删除的或者不可访问的(例如,是private的),则类的合成析构函数被定义为删除的
  • 如果类的某个成员的拷贝构造函数是删除的或不可访问的,则类的合成拷贝构造函数被定义为删除的,如果类的某个成员析构函数是删除的或不可访问的,,则类合成的拷贝构造函数也被定义为删除的。
  • 如果类的某个成员的拷贝赋值运算符是删除的或不可访问的,或者是类有一个const的或引用成员,则类的合成拷贝赋值运算符被定义为删除的
  • 如果类的某个成员的析构函数是删除的或是不可访问的,或是类有一个const成员,它没有类内初始化器且其类型未显示定义默认构造函数 ,或是类有一个引用成员,它没有类内初始化器,则该类的默认构造函数被定义为删除的
本质上,如果一个类有数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数被定义为删除的。
一个成员有删除的或不可访问的析构函数会导致合成的默认和拷贝构造函数被定义为删除的。




上一篇:【C++ Primer 第十三章】4. 拷贝控制示例


下一篇:本地的手机号码归属地查询-oracle数据