blueyi's notes

Follow Excellence,Success will chase you!

0%

C++学习笔记之拷贝控制

1.一个类通过定义五种特殊的成员函数来控制这些操作,包括:拷贝构造函数(copy constructor)、拷贝赋值运算符(copy-assignment operator)、移动构造函数(move constructor)、移动赋值运算符(move-assignment operator)和析构函数(destructor)。拷贝和移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么。拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么。析构函数定义了当此类型对象销毁时做什么。其中移动操作都是在新标准中引用的。如果我们没有手动定义这些操作,则编译器会为我们自动生成。

2.拷贝构造函数:该构造函数的第一个参数是自身类类型的引用(该参数几乎总是一个const的引用),且任何额外参数都有默认值,则此构造函数是拷贝构造函数。编译器会默认为所有类定义一个合成拷贝构造函数。

3.以下情况下会调用拷贝构造函数:

  • 使用=进行定义并初始化变量时
  • 将一个对象作为实参传递给一个非引用类型的形参
  • 从一个返回类型为非引用类型的函数返回一个对象
  • 用花括号列表初始化一个数组中的元素或一个聚合类中的成员

4.构造函数中,成员的初始化是在函数体执行之前完成的,且按照它们在类中出现的顺序进行初始化。

5.拷贝赋值运算符就相当于重载赋值运算符,赋值运算符就是一个名为operator=的函数,如果一个运算符是一个成员函数,其左侧运算对象就绑定到隐式的this参数,其右侧运算对象作为显式参数传递。拷贝赋值运算符接受一个与其所在类相同类型的参数:

1
2
3
4
5
class Foo {
public:
Foo& operator=(const Foo&);//赋值运算符
//...
};

为了与内置类型的赋值保持一致,赋值运算符通常返回一个指针向其左侧运算对象的引用。

6.析构函数执行与构造函数相反的操作:构造函数初始化对象的非static数据成员,还可能做一些其他工作;析构函数释放对象使用的资源,并销毁对象的非static数据成员。析构函数是类的一个成员函数,名字由波浪号接类名构成。它没有返回值,也不接受参数:

1
2
3
4
class Foo {
public:
~Foo(); //析构函数
};

析构函数不接受参数,所以无法被重载,对于一个给定类,只会有唯一一个析构函数。

7.析构函数中,首先执行函数体,然后销毁成员,成员按初始化顺序的逆序销毁。在对象最后一次使用之后,析构函数的函数体可以执行类设计者希望执行的任何收尾工作,通常析构函数释放对象在生存期分配的所有资源。而析构函数不存在类似构造函数中初始化列表的东西来控制成员如何销毁,析构部分是隐式的。所以成员的销毁完成依赖于成员的类型,销毁类类型的成员时需要执行成员自己的析构函数,内置类型没有析构函数,所以销毁内置类型成员时什么也不需要做。

8.无论何时一个对象被销毁时,都会自动调用其析构函数,具体有:

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

拷贝控制函数举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class HasPtr {
public:
//构造函数,使用传递的string初始ps,0初始化i
HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0) {}
//拷贝构造函数,动态分配一个新的string将值拷贝到ps指向的位置,而不是ps本身的位置
HasPtr(const HasPtr& hp) : ps(new std::string(*hp.ps)), i(hp.i) {}
//拷贝赋值运算符
HasPtr& operator=(const HasPtr& hp) {
//拷贝底层的string,即新建一个string对象,并将来的this.ps指向新的string对象
std::string* new_ps = new std::string(*hp.ps);
delete ps; //释放旧内存
ps = new_ps; //从右侧运算对象拷贝数据到本对象
i = hp.i;
return *this; //返回本对象
}
//析构函数
~HasPtr() { delete ps; }
private:
std::string *ps;
int i;
};

赋值运算符需要注意:

  • 如果将一个对象赋予它自身,赋值运算符必须能正确工作。
  • 大多数赋值运算符组合了析构函数和拷贝构造函数的工作。
    所以编写一个赋值运算符的好模式是先将右侧运算对象拷贝到一个局部临时对象中,当拷贝完成后,销毁左侧运算对象的现有成员就是安全的了,然后将数据从临时对象拷贝到左侧运算对象的成员中即可。

10.如果一个类需要自定义析构函数,几乎可以肯定它也需要自定义拷贝赋值运算符和拷贝构造函数。

11.可能通过将拷贝控制成员定义为=default来显式地要求编译器生成合成的版本。eg:

1
2
3
4
5
6
7
8
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。

12.为了阻止拷贝,大多数类应该定义默认构造函数、拷贝构造函数和拷贝赋值运算符,无论是隐式地还是显式地。因为如果不手动定义,编译器会为它生成合成的版本。

13.在C++11中可以手动在函数的参数列表后面加上=delete来指出我们希望将它定义为删除的,将拷贝构造函数和拷贝赋值运算符定义为删除的函数可以阻止拷贝。因为删除函数是这样一种函数:我们虽然声明了它们,但不能以任何方式使用它们,所以它可以阻止合成的拷贝构造函数。析构函数不能是删除的成员。新标准发布之前,类是通过将拷贝构造函数和拷贝赋值运算符声明为private来阻止拷贝,但现在推荐使用=delete而不是private。

15.对于行为像值的类,在类管理的资源中,每个对象都应该拥有一份自己的拷贝。

行为像指针的类

对于行为类似指针的类,我们需要为其定义拷贝构造函数和拷贝赋值运算符,来拷贝指针成员本身而不是它指向的string。eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//使用use记录拷贝次数
class HasPtr {
public:
//构造函数分配新的string和新的计数器,将计数器置为1
HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0), use(new std::size_t(1)) {}
//拷贝构造函数拷贝所有三个数据成员,并递增计数器
HasPtr(const HasPtr &p) : ps(p.ps), i(p.i), use(p.use) {++*use;}
HasPtr& operator=(const HasPtr&);
~HasPtr();
private:
std::string *ps;
int i;
std::size_t *use;
};

HasPtr::~HasPtr()
{
if (--*use == 0) { //如果引用计数变为0
delete ps; //释放string内存
delete use; //释放计数器内存
}
}

HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
++*rhs.use; //递增右侧运算对象的引用计数
if (--*use == 0) { //然后递减本对象的引用计数
delete ps; //如果没有其他用户
delete use; //释放本对象分配的成员
}
ps = rhs.ps; //将数据从rhs拷贝到本对象
i = rhs.i;
use = rhs.use;
return *this; //返回本对象
}

使用swap实现拷贝赋值运算符

使用拷贝和交换的赋值运算符自动就是异常安全的,且能正确处理自赋值。 eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class HasPtr {
friend void swap(HasPtr&, HasPtr&);
public:
HasPtr& operator=(const HasPtr);
//...
};
inline void swap(HasPtr &lhs, HasPtr &rhs)
{
using std::swap;
swap(lhs.ps, rhs.ps); //交换指针,而不是string数据
swap(lhs.i, rhs.i); //交换int成员
}
//注意rhs是按值传递的,意味着HasPtr的拷贝构造函数将右侧运算对象中的string拷贝到rhs
HasPtr& HasPtr::operator=(HasPtr rhs)
{
//交换左侧运算对象和局部变量rhs的内容
swap(*this, rhs); //rhs现在指向本对象曾经使用的内存
return *this; //rhs被销毁,从而delete了rhs中的指针
}

拷贝控制实例

Message_Folder.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/*
* Message_Folder.h
* Copyright (C) 2016 <@BLUEYI-PC>
*
* Distributed under terms of the MIT license.
*/

#ifndef MESSAGE_FOLDER_H
#define MESSAGE_FOLDER_H
#include <string>
#include <set>

class Folder;
class Message
{
friend void swap(Message &, Message &);
friend void swap(Folder &, Folder &);
friend class Folder;
public:
explicit Message(const std::string str = "") : contents(str) {}
Message(const Message&);
Message & operator=(const Message&);
~Message();
void save(Folder&);
void remove(Folder&);
void printtxt();
void printfolders();
private:
std::string contents;
std::set<Folder *> folders;

void add_to_folders(const Message&);
void remove_from_folders();

void addFldr(Folder *f) {folders.insert(f);}
void remFldr(Folder *f) {folders.erase(f);}
};

void swap(Message &, Message &);
void swap(Folder &, Folder &);

class Folder
{
friend void swap(Message &, Message &);
friend void swap(Folder &, Folder &);
friend class Message;
public:
Folder() = default;
Folder(const Folder&);
Folder & operator=(const Folder&);
~Folder();
void printmsg();

private:
std::set<Message *> messages;

void add_to_messages(const Folder&);
void remove_from_messages();

void addMsg(Message *m) {messages.insert(m);}
void remMsg(Message *m) {messages.erase(m);}
};

#endif /* !MESSAGE_FOLDER_H */

Message_Folder.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
/*
* Message_Folder.cpp
* Copyright (C) 2016 <@BLUEYI-PC>
*
* Distributed under terms of the MIT license.
*/

#include "Message_Folder.h"

#include <iostream>

void swap(Message &lhs, Message &rhs)
{
using std::swap;
for (auto f : lhs.folders)
f->remMsg(&lhs);
for (auto f : rhs.folders)
f->remMsg(&rhs);

swap(lhs.folders, rhs.folders);
swap(lhs.contents, rhs.contents);

for (auto f : lhs.folders)
f->addMsg(&lhs);
for (auto f : rhs.folders)
f->addMsg(&rhs);
}

void swap(Folder &lhs, Folder &rhs)
{
using std::swap;
for (auto m : lhs.messages)
m->remFldr(&lhs);
for (auto m : rhs.messages)
m->remFldr(&rhs);

swap(lhs.messages, rhs.messages);

for (auto m : lhs.messages)
m->addFldr(&lhs);
for (auto m : rhs.messages)
m->addFldr(&rhs);
}
void Message::save(Folder &f)
{
folders.insert(&f);
f.addMsg(this);
}
void Message::remove(Folder &f)
{
folders.erase(&f);
f.remMsg(this);
}
void Message::add_to_folders(const Message &m)
{
for (auto f : m.folders)
f->addMsg(this);
}
void Message::remove_from_folders()
{
for (auto f : folders)
f->remMsg(this);
folders.clear();
}
Message::Message(const Message &m) : contents(m.contents), folders(m.folders)
{
add_to_folders(m);
}
Message::~Message()
{
remove_from_folders();
}
Message &Message::operator=(const Message &rhs)
{
remove_from_folders();
contents = rhs.contents;
folders = rhs.folders;
add_to_folders(rhs);
return *this;
}
void Folder::add_to_messages(const Folder &f)
{
for (auto m : f.messages)
m->addFldr(this);
}
Folder::Folder(const Folder &f) : messages(f.messages)
{
add_to_messages(f);
}
void Folder::remove_from_messages()
{
for (auto m : messages)
m->remFldr(this);
messages.clear();
}
Folder::~Folder()
{
remove_from_messages();
}
Folder &Folder::operator=(const Folder &rhs)
{
remove_from_messages();
messages = rhs.messages;
add_to_messages(rhs);
return *this;
}

void Message::printtxt()
{
std::cout << "contents: " << contents << std::endl;
}

void Message::printfolders()
{
std::string str("************************");
std::cout << str << std::endl;
std::cout << "All folders messages: " << std::endl;
for (auto f : folders)
{
f->printmsg();
}
}

void Folder::printmsg()
{
std::cout << "Msg: " << std::endl;
for (auto m : messages)
std::cout << m->contents << std::endl;
}

int main()
{
Message me("Hello world!");
Folder* tags = new Folder();
me.save(*tags);
me.printtxt();
me.printfolders();
tags->printmsg();
return 0;
}

19.定义动态内存管理类需要使用到allocator类及其成员函数construct,allocate,deallocate等。

20.C++11增加了右值引用来支持对象移动。头文件utility中的move函数可以显示地来获得绑定到左值上的右值引用。

左值引用

通过一个&符号定义的引用即为左值引用,所有的变量都是左值。左值引用只能绑定到左值上,具体左值有返回左值引用的函数,连同赋值、下标、解引用和前置递增递减运行符,我们可以将左值引用绑定到这类表达式上。而右值引用刚好相反

右值引用

1.右值引用与左值引用相反,例如返回非引用类型的函数,连同算法、关系、位以及后置递增/递减运算符都生成右值,我们不能将一个左值引用绑定这类表达式上,但可以将一个const的左值引用或一个右值引用绑定到这类表达式上。同样右值引用只能绑定到右值上。左值有持久的状态,如变量。而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。如:

1
2
3
4
5
6
7
int i = 4;  //变量i是个左值
int &ri = i; //将一个左值引用绑定到一个左值上,变量ri同样为左值
int &&rri = ri; //错误:不能将右值引用绑定到一个左值
int &r2 = i * 3; //错误:i * 3是一个右值
int &&rr2 = i * 3; //正确:将一个右值引用绑定右值
const int &r3 = i * 3; //正确:将一个const的引用绑定到右值
int &&rr4 = rr2; //错误:变量rr2为左值,虽然rr2为右值引用,但其本身是个变量,变量是左值

2.通过标准库move函数可以获得一个绑定到左值上的右值引用,如:

1
2
int li= 23;
int &&rli = std::move(li);

调用move就意味着承诺:除了对li赋值或销毁外,我们不再使用它,调用move后,我们不能对移后源对象的值做任何假设,也就是说移动之后,最好不要再使用源对象。move定义在头文件utility中。

区分一个左值与右值的一个简单方法是:看能不能对表达式取地址,如果能,就是左值,如果不能,就是右值。

移动构造函数

移动构造函数类似拷贝构造函数,不同的是它们从给定对象移动资源而不是拷贝资源。移动构造函数的定义为第一个参数是该类类型的一个引用,不同地拷贝构造函数的是这个引用参数在移动构造函数中是一个右值引用,与拷贝构造函数相同的是任何额外的参数都必须有默认实参。除了完成移动,移动构造函数还必须确保移后源对象处于这样一个状态:销毁它是无害的,特别是一旦完成移动,源对象必须不再指向被移动的资源,即这些资源的所有权已经归属新创建的对象。因为移动后新对象将使用源对象指向的内存。eg:

1
2
3
4
5
6
StrVec::StrVec(StrVec &&s) noexcept  //移动操作不应该抛出任何异常
//成员初始化器接管s中的资源
: elements(s.elements), first_free(s.first_free), cap(s.cap)
{
s.elements = s.first_free = s.cap = nullptr; //令s进入这样的状态——对其调用析构函数是安全的
}

与拷贝构造函数不同,移动构造函数不分配任何新内存,它接管给定的StrVec中的内存,接管后它将约定对象中的指针都置为nullptr。
关于noexcept,noexcept出现在参数列表和初始化列表开始的冒号之间。我们必须在类头文件的声明和定义中都指定noexcept,不抛出异常的移动构造函数和移动赋值运算符都必须标记为noexcept。这两个函数并不是必须指定为noexcept,只是为了与标准库进行交互。例如标准库容器能对异常发生时其自身的行为提供保障,也就是说以vector举例,除非vector知道元素类型的移动构造函数不会抛出异常,否则在重新分配内存的过程中,它就必须使用拷贝构造函数而不是移动构造函数。因为移动一个对象通常会改变它的值,如果新分配过程中使用移动构造函数,且在移动了部分元素之后抛出一个异常,此时旧空间中的移动源元素已经改变了,而新空间中未构造的元素可能还不存在,此时vector将不能满足自身保存不变的要求。但同样的情况下拷贝构造函数却可以,所以拷贝构造函数可以不需要noexcept。另外一点移动操作通常也不会抛出异常。所以 为了标准库能够正常调用移动构造函数,必须显示地告诉标准库我们的移动构造函数可以安全地使用。

移动赋值运算符

与赋值运算符类似,只是传递的对象需要是右值引用,并应该标记为noexcept

1
2
3
4
5
6
7
8
9
10
11
12
13
StrVec &StrVec::operator=(StrVec &&rhs) noexcept
{
//检测自赋值
if (this != &rhs) {
free(); //释放已有元素
elements = rhs.elements; //从rhs接管资源
first_free = rhs.first_free;
cap = rhs.cap;
//将rhs置于可析构状态
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}

在移动操作之后,移后源对象必须保持有效的、可析构的状态,但是用户不能对其值做任何假设

合成的移动操作

只有当一个类没有定义任何自己版本的拷贝控制成员,且它的所有数据成员都能够移动构造或移动赋值时,编译器才会为它合成移动构造函数或移动赋值运算符。与拷贝操作不同,移动操作永远不会隐式定义为删除的函数。但如果显示地要求编译器生成=default的移动操作,且编译器不能移动所有成员时,编译器会将移动操作定义为删除的函数。
定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作。否则,这些成员默认地被定义为删除的。

如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数。如果一个类有一个可用的拷贝构造函数而没有移动构造函数,则其对象是通过拷贝构造函数来进行“移动”的,移动赋值运算符的情况类似。

移动迭代器

1.C++11中定义了一种移动迭代器适配器,一个移动迭代器通过改变给定迭代器的解引用运算符的行为来适配此迭代器,移动迭代器的解引用运算符生成一个右值引用。标准库头文件iterator中的make_move_iterator函数可将一个普通迭代器转换为一个移动迭代器,些函数接受一个迭代器参数,返回一个移动迭代器。由于移动迭代器支持正常的迭代器操作,所以可以将一个移动迭代器传递给算法。memory头文件中的uninitialized_copy函数可以对元素进行拷贝。eg:

1
2
3
4
5
6
7
8
9
10
11
void StrVec::rellocate()
{
auto newcapacity = size() ? 2 * size() : 1;
auto first = alloc.allocate(newcapacity);
//移动元素
auto last = uninitialized_copy(make_move_iterator(begin()), make_move_iterator(end()), first);
free(); /释放旧空间
elements = first; //更新指针
first_free = last;
cap = elements + newcapacity;
}

右值引用的其他用法

1.可以为成员函数提供拷贝和移动版本,既可以同时提供两个版本,一个版本接受一个指向const的左值引用,另一个版本接受一个指向非const的右值引用。eg:

1
2
3
4
5
class A {
public:
void push_back(const std::string&); //拷贝元素
void push_back(std::string&&); //移动元素
};

2.可以通过向成员函数参数列表后面放置引用限定符(&或&&)来分别指出this可以指向一个左值或右值,类似const限定符,引用限定符只能用于(非static)成员函数,且必须同时现出在函数的声明和定义中。如果一个函数同时用const和引用限定符,则引用限定符必须出现在const的后面。

3.引用限定符也可以用于区分重载版本,与const不同的是const用于区分时是根据是否有const来区分,而引用限定符则是通过是左值引用还是右值引用,即是&还是&&来区分。如果我们定义两个或两个以上具有相同名字和相同参数列表的成员函数,就必须对所有函数都加上引用限定符,可以是&或&&,或者所有都不加,不能有的加,有的不加。

4.非常量引用的初始值必须为左值,如果要传递给函数引用类型的形参一个右值,必须以const &的形式,将形参定义为常量。如:

1
int fun(int &);

这样调用是错误的

1
fun(0);

但定义为形如:

1
int func(const int &);

调用:

1
func(0);

没有问题

Welcome to my other publishing channels