layout: post
title: C++prime读书笔记(三)拷贝控制、运算重载与类型转换、对象继承、模板与泛型
description: C++prime读书笔记(三)拷贝控制、运算重载与类型转换、对象继承、模板与泛型
tag: 读书笔记
当定义一个类时,我们显式或隐式地指定在此类型的对象拷贝、移动、赋值和销毁时做什么,这些操作统称为拷贝控制操作,一个类通过定义5中特殊的成员函数来控制这些操作,包括拷贝构造函数、拷贝赋值函数、移动构造函数、移动赋值运算符和析构函数
。
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数
class Foo
{
public:Foo(); // 默认构造Foo(const Foo&); // 拷贝构造
};
注意:
1、当我们没有为类声明任何构造函数时,编译器为我们合成默认构造函数。
但拷贝构造不同,即使我们定义了其他构造函数,编译器也会为我们合成一个拷贝构造函数
2、拷贝构造接收的参数必须是自身类类型的引用,如果参数不是引用类型,则调用永远也不会成功,为了调用拷贝构造函数,我们必须拷贝它的实参,但为了拷贝实参,我们又要调用拷贝构造函数,如此无限循环。
直接初始化时,我们实际上要求编译器使用普通的函数匹配,而使用拷贝初始化时,我们要求编译器将右侧运算对象拷贝到正在创建的对象,如果需要的话还会进行类型转换。
string dots(10, ','); //直接初始化
string s(dots); // 直接初始化
string s2 = dots; // 拷贝初始化
string null-book = "99999=99999"; // 拷贝初始化
string nines = string(100, '9'); // 拷贝初始化
拷贝初始化不仅在使用=
定义变量时会发生,在下列情况下也会发生:
注: 1、某些类型还会对他们所分配的对象使用拷贝初始化,例如当我们使用insert或者push成员时,容器会对元素进行拷贝初始化,与之对应用emplace成员创建的元素都进行直接初始化
2、如果我们希望使用explicit构造函数就必须显式地使用直接初始化,不可使用隐式地拷贝初始化转换。
与类控制对象如何初始化一样,类也可以控制对象如果进行赋值。
与处理拷贝构造函数一样,如果一个类未定义自己的拷贝赋值运算符,编译器会为它生成一个合成拷贝赋值运算符。作为一个例子,下边的代码等价于Sales_data的合成拷贝赋值运算符,合成拷贝赋值运算符返回一个指向其左侧运算对象的引用。
Sales_data&
Sales_data::operator=(const Sales_data &rhs)
{bookNo = rhs.bookNo;units_sold = rhs.units_sold;revenue = rhs.revenue;return *this;
}
析构函数释放对象使用的资源,销毁对象的非static数据成员。与普通指针不同,智能指针是类类型,所以具有析构函数。
以下情况会调用析构函数:
当一个类未定义自己的析构函数时,编译器会为它定义一个合成析构函数,类似拷贝构造函数和拷贝赋值运算符,在(空)析构函数体执行完毕后,成员会被自动销毁,析构函数体自身并不直接销毁成员,成员是在析构函数体之后隐含的析构阶段中被销毁的,在整个对象销毁的过程中,析构函数体是作为成员销毁步骤之外的另一部分而进行的
C++中有三个基本操作可以控制类的拷贝操作:拷贝构造函数、拷贝赋值运算符和析构函数,在C++11新标准下,一个类还可以定义一个移动构造函数
和一个移动赋值函数
。
当我们决定一个类是否要定义它自己版本的拷贝控制成员时,一个基本原则是首先确定这个类是否需要一个析构函数,通常对于析构函数的需求要比对拷贝构造函数或者赋值运算符的需求更加明显。如果这个类需要一个析构函数,我们几乎可以确定它也需要一个拷贝构造函数和一个拷贝赋值运算符
例如下边这个例子:HasPtr中有指针数据成员,因此需要析构函数来释放指针。另一方面,由于包含指针成员,如果使用合成的拷贝构造和拷贝赋值运算符,这些函数简单拷贝指针成员,意味着多个HasPtr对象可能指向相同的内存。
class HasPtr{
public:HasPtr(const std::string &s = std::string()): ps(new std::string(s)), i(0) {}~HasPtr(){delete ps;}// 错误:HasPtr还需要一个拷贝构造函数和一个拷贝赋值运算符HasPtr& operator=(const HasPtr &hasptr){ps = new std::string(*(hasptr.ps));i = hasptr.i;return *this;} HasPtr(const &hasptr){ps = new std::string(*(hasptr.ps));i = hasptr.i;return *this;}
private:std::string *ps;int i;
};
三/五法则的第二条是“如果一个类需要一个拷贝构造函数,几乎可以肯定它也需要一个拷贝赋值运算符,反之亦然——如果一个类需要一个拷贝赋值运算符,几乎可以肯定它也需要一个拷贝构造函数
。”
我们可以通过将拷贝控制成员定义为=default
来显式地要求编译器生成合成的版本,当我们在类内使用=default修饰成员时,合成的函数将隐式地声明为内联
的(就像任何其他类内声明的成员函数一样)。如果不希望合成的成员是内联函数,应该只对成员的类外定义使用=default,就像下面例子中对于拷贝赋值运算符=
所做的那样。
class Sales_data
{
public:// 拷贝控制成员使用defaultSales_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。
对于某些类来讲拷贝和赋值没有合理的意义,因此在定义这些类时必须采用某种机制阻止拷贝或赋值。例如iostream类阻止了拷贝,以避免多个对象写入或读取相同的IO缓冲。为了阻止拷贝,可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数,这种删除函数的意思是:我们虽然声明了它们,但不能以任何方式使用它们
。=delete通知编译器,我们不希望定义这些成员。与=default不同,=delete必须出现在函数第一次声明时,且析构函数不能是删除的成员
struct NoCopy
{NoCopy() = default(); // 使用合成默认构造函数NoCopy(const NoCopy&) = delete; // 阻止拷贝NoCopy &operator=(const NoCopy&) = delete; // 阻止赋值~NoCopy() = default; // 使用合成的析构函数
};
注:合成的拷贝控制成员可能是删除的,如果一个类有数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数将被定义为删除的。