今天正式进入Part III的学习:Tools for Class Authors
Part III的内容都和class有着紧密的关系,它深入拓展了第七章的内容。
如果要使用基本的C++程序,Part I和Part II的内容已经够用了;
如果要更加深入的了解C++,还是要进行Part III的学习。
Part III共有四个章节,首先要学习的是第一个章节:Chapter 13 Copy Control
copy constructor
copy-assignment operator
move constructor
move-assignment operator
destructor
这五个操作统称为copy control。
一般来说,编译器为我们定义了默认的copy constrol,但某些时候我们需要定义自己的版本。
13.1 Copy, Assign, and Destroy
----------------------------------------------------------------
1. The Copy Constructor
第一个参数是类的类型的引用,而且其他的参数都为默认值。
比如:
class Foo {
public:
Foo();
Foo(const Foo&);
};
第一个参数一定要是reference,且大多数时候是const的。
如果我们定义了其他的构造函数,那么就一定要显式的把默认构造函数写出来;
如果我们定义了其他的copy构造函数,编译器仍然会为我们生成默认的综合构造函数。
类的成员类会调用自身的copy函数,build-in变量则会直接复制;
类的成员array不能直接复制,需要在copy constuctor里面一个一个的进行操作。
string dots(10, '.'); // direct initialization
string s(dots); // direct initialization
string s2 = dots; // copy initialization
string null_book = "9-999-99999-9"; // copy initialization
string nines = string(100, '9'); // copy initialization
direct initialization调用的是构造函数;
copy initialization通常调用的是copy构造函数。
当我们使用某对象作为非引用的参数,或者使用某对象作为非引用的返回,或者brace initialize,
都会使用到copy initialization。
库容器的copy、insert、push成员使用了copy initialize,
而emplace则是direct initialize。
如果copy构造函数的参数不是引用的话,那么它本身就需要读取这个参数(而不是直接使用),
而读取并copy的过程又需要copy构造函数,这样就陷入了无穷的循化中。
* copy操作本身经常被使用,而且它的细节处理的内容非常繁琐,在实际中要多调试。
2. The Copy-Assignment Operator
编译器提供了默认的版本,我们可以定义自己的版本。
比如:
class Foo {
public:
Foo();
Foo(const Foo&);
Foo& operator=(const Foo&);
};
这里的函数名称为operator=,它必须是类的成员函数,返回值绑定到了this指针上,
一般来说,assignment操作符必须返回一个引用作为左操作数。
copy-assignment operator使用同类型的类作为参数。
编译器提供了默认的版本,我们可以提供自定义的版本来禁止assign操作,或者执行其他的操作。
编译器提供了默认的版本,会使用assign操作来复制nonstatic成员变量。
3. The Destructor
析构函数做的是和构造函数相反的事情,释放nonstatic成员以及其他的资源。
比如:
class Foo {
public:
Foo();
Foo(const Foo&);
Foo& operator=(const Foo&);
~Foo();
};
析构函数以~开始,参数表为空,因此它不可被重载。
每个类都只有一个析构函数。
构造函数是先按照类的存储顺序初始化变量,然后执行函数体;
析构函数是先执行函数体,再释放变量,释放的顺序和存储的顺序反向。
析构函数会删除指针类变量,但不会释放指针所指向的存储空间。
析构函数会删除smart指针变量,smart指针会控制资源的释放。
一般来说,凡是对象由于越过了scope而被释放的地方,析构函数会被自动的被调用。
我们不需要定义所有的copy control操作,需要什么定义什么就好,
但是一般来说,如果需要其中一个copy control操作,很可能也需要其他的操作。
如果一个类需要自定义的destructor函数,那它基本上是肯定需要copy构造和copy assign;
如果一个类需要copy构造,那它肯定需要copy assign,反之亦然(但不一样需要析构)。
怎样禁止copy操作?
class Foo {
public:
Foo() = default;
Foo(const Foo&) = delete;
Foo& operator=(const Foo&) = delete;
~Foo() = default;
};
“= delete”告诉编译器,这个操作是被禁止的。
编译器可能会将默认的综合函数设置成“= delete”的,有以下几种情况:
* 如果类成员有deleted或者不可访问的析构函数,那么这个类的析构函数也是deleted。
* 如果类成员有deleted或者不可访问的copy函数,那么这个类的copy函数也是deleted;
或者析构函数是deleted或者不可访问,也是这样。
* 如果类成员有deleted或者不可访问的copy assign函数,那么类的copy assign为deleted;
或者类成员为const或者引用类型,也是这样。
* 如果类有未in-class初始化的引用成员,或者没有显式构造的const成员,析构函数deleted。
13.2 Copy Control and Resource Management
----------------------------------------------------------------
当我们设计copy control的时候,要理解我们所设计的功能的实际物理含义是什么。
copy assign的功能设计很关键:
* 自己给自己赋值,也必须正常工作(在函数中保存副本)。
* assign一般会完成析构函数和copy函数的工作。
13.3 Swap
----------------------------------------------------------------
swap由两次copy组成。
除了创建对象的temp来copy之外,我们还可以copy指针。
我们也可以定义自己的swap函数:
class Foo {
public:
Foo();
friend void swap(Foo&, Foo&);
private:
int i;
string *ps;
};
inline void swap(Foo &lhs, Foo &rhs)
{
using std::swap;
swap(lhs.ps, rhs.ps);
swap(lhs.i, lhs.i);
}
swap函数不是必须的,但是它可以极大的优化需要allocate资源的类。
assignment操作可以使用copy函数实现,也可以使用swap函数实现。
13.4 A Copy-Control Example
----------------------------------------------------------------
13.5 Classes That Manage Dynamic Memory
----------------------------------------------------------------
好吧,这两个章节分别设计了自己的example,
实际设计的时候再来参考它们,现在还是继续学习接下来的内容。
13.6 Moving Objects
----------------------------------------------------------------
有时候,我们并不需要copy,而是需要move;
有时候,类似IO或者unique_ptr的类,不能被copy,只能被move。
库容器、string和shared_ptr类支持move操作,也支持copy操作;
IO和unique_ptr只支持move操作。
这里我们引进了一个新的符号:&&
它表示rvalue references,它只能用来表示即将被释放的对象,这样我们才能move它们。
一般来说,lvalue代表的是对象的identity,rvalue代表的是对象的值。
lvalue是永久的,而rvalue是暂时的。
变量都是lvalues。
因此我们不能将一个rvalue引用绑定到一个rvalue引用上:
int &&rr1 = 42; // ok: literals are rvalues
int &&rr2 = rr1; // error: the expression rr1 is an lvalue!
int &&rr3 = std::move(rr1); // ok
我们可以释放一个moved-from对象,或者重新给它赋值,但不能使用这个moved-from对象的值。
假设有:
int f();
vector vi(100);
那么应该:
int &&r1 = f();
int &r2 = vi[0];
int &r3 = r1;
int &&r4 = vi[0] * f();
其实判断的条件很简单,看右侧的返回值是identify还是value。
现在我们可以来设计第一个move函数了:
class StrVec {
StrVec(StrVec &&s) noexcept;
private:
string *elements;
string *first_free;
string *cap;
};
StrVec::StrVec(StrVec &&s) noexcept
: elements(s.elements), first_free(s.first_free), cap(s.cap)
{
s.elements = s.first_free = s.cap = nullptr;
}
noexcept是非常关键的,它告诉编译器不要产生异常,因为moved-from对象会被改变。
不论我们有没有定义自己的copy构造和copy-assignment函数,编译器始终会为我们生成它们;
当我们定义了自己的copy构造、copy-assignment或者destructor函数,编译器就不会生成move。
只有当我们没有定义这些copy*函数,并且每个nonstatic成员都可被move,编译器才会生成默认的move。
move从来不会隐式的被定义为deleted。
move不可用的时候,就会使用copy。
因为moved-from对象的状态会被改变,因此调用std::move()函数是一个非常危险的操作。
我们必须保证,我们调用它的时候并没有其他的用户会使用这个对象。
* 此章需加深理解。
文章评论(0条评论)
登录后参与讨论