前言
该系列是《C++Primer第五版》的笔记,包含本人认为值得记录和整理的主要的知识点,并不是全部内容,也不是具体的内容。
该系列文章的作用应该是作为复习或预习的参考,有哪些知识点忘记或想学,可以大致浏览下该文章,然后再去书中寻找详细解答。(本系列文章基本是按书本顺序罗列的知识点,便于大家去书中寻找)
所以看该文章前,需要有一定的C++基础,否则阅读起来可能有困难。
本文大致整理了第十三章的知识点,涉及到C++关于类的拷贝控制内容(拷贝构造、拷贝赋值、析构、移动构造、移动赋值)、动态内存分配中的拷贝控制、右值引用的知识,虽然内容非常多,但比较重要!
链接目录
- 第二章:变量与基本类型
- 第三章:字符串、向量和数组
- 第四章:表达式
- 第五章:语句
- 第六章:函数
- 第七章:类
- 第八章:IO库
- 第九章:顺序容器
- 第十章:泛型算法
- 第十一章:关联容器
- 第十二章:动态内存
- 第十三章:拷贝控制
- 第十四章:重载运算与类型转换
- 第十五章:面向对象程序设计
- 第十六章:模板与泛型编程
- 第十七章:标准库特殊设施
- 第十八章:用于大型程序的工具
- 第十九章:特殊工具与技术
拷贝构造函数
拷贝构造函数:第一个参数是自身类类型的引用,其他额外参数都有默认值。通常第一个参数是const
引用,而且不应该是explicit
的(拷贝函数经常被隐式使用)。
合成拷贝构造函数:将每个非static
成员拷贝到正在创建的对象中。内置类型直接拷贝,类类型会调用拷贝构造函数。
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 &orig):
bookNo(orig.bookNo),
units_sold(orig,units_sold),
revenue(orig.revenue){}
拷贝初始化:
string dots(10, '.');//直接初始化
string s(dots);//直接初始化
string s2 = dots;//拷贝初始化
string null_book = "9-999-99999";//拷贝初始化
string nines = string(100, '9');//拷贝初始化
拷贝构造函数调用的情况:
- 用
=
定义变量。 - 将一个对象作为实参传递给一个非引用类型的形参。
- 从一个返回类型为非引用类型的函数返回一个对象。
- 用花括号列表初始化一个数组中的元素或一个就聚合类中的成员。
string null_book = "a";//拷贝初始化
string null_book("a");//略过了拷贝构造函数
编译器可以绕过拷贝构造函数,但拷贝构造函数仍然是需要存在且可访问的,并不能是private
。
拷贝赋值运算符
将=
重载为拷贝赋值运算符:如果类未定义,编译器会自动合成一个。
析构函数
析构函数释放对象使用的资源,并销毁对象非static
数据成员。通常编译器的合成析构函数就能完成任务,此时只需要声明一个析构函数,其函数体为空。但动态分配的对象、指针或引用需要注意,并不会自动释放。
class Foo{
public:
~Foo();//析构函数,没有返回值和参数,也不能被重载
};
隐式销毁一个内置指针类型的成员不会delete
它所指向的对象,销毁的仅仅是指针。但智能指针是类类型,具有析构函数,会被自动销毁。
内置类型没有析构函数,所以销毁内置类型什么都不需要做。
析构函数调用:
- 变量离开其作用域。
- 当一个对象被销毁时,其成员被销毁。
- 容器被销毁时,其元素被销毁。
- 动态分配的对象,当其指针使用
delete
时被销毁。 - 对于临时对象,当创建它的完整表达式结束时被销毁。
当指向一个对象的引用或指针离开作用域时,析构函数不会执行。
三/五法则
一个类需要析构函数->几乎肯定需要拷贝构造函数和拷贝赋值运算符。
当一个类内有动态内存分配时,需要析构函数手动对其指针进行释放操作。此时如果使用默认的拷贝构造函数,仅进行指针的复制,当对象进行析构时,会多次delete
该指针所指向的内存。
一个类需要拷贝构造函数->几乎肯定需要拷贝赋值运算符。反之亦然。
使用=default
显式要求编译器生成合成的版本。合成的函数将隐式地声明为内联的(任何声明在类内部的函数都是内联的),如果不希望是内联的,应该只对类外定义使用default
。
class Sales_data{
public:
Sales_data() = default;
Sales_data(const Sales_data&) = default;
~Sales_data() = default;
};
阻止拷贝
定义删除的函数:不能以任何方式使用它们,在参数列表后加=delete
(必须出现在函数第一次声明的时候)。另外,析构函数不能是删除的函数,否则会导致对象无法销毁。
struct NoCopy{
NoCopy(const NoCopy&) = delete;//阻止拷贝
NoCopy &operator=(const NoCopy&) = delete;//阻止赋值
};
合成的拷贝控制成员可能是删除的:
- 如果类的某个成员的析构函数是删除的或不可访问的,则类的合成析构函数被定义为删除的。
- 如果类的某个成员的拷贝构造函数是删除的或不可访问的,则类的合成拷贝构造函数被定义为删除的。如果析构函数是删除的或不可访问的,合成拷贝构造函数也是删除的。
- 如果类的某个成员的拷贝赋值运算符是删除的或不可访问的,或类有一个
const
的或引用成员,则类的合成拷贝赋值运算符被定义为删除的。 - 如果类的某个成员的析构函数是删除的或不可访问的,或类有一个引用成员,没有类内初始化器,或类有一个
const
成员,没有类内初始化器且其类型未显式定义默认构造函数,则类的默认构造函数被定义为删除的。
private
拷贝控制:新标准之前,将拷贝控制成员定义成private
阻止拷贝进行,但友元和成员函数仍能访问,此时只声明拷贝控制成员但不定义(试图访问一个未定义的成员会导致链接错误)。
拷贝控制和资源管理
类的行为像值:拷贝值对象,副本和原对象独立互不影响。
类的行为像指针:共享状态,拷贝指针对象,副本和原对象使用相同的底层数据。
行为像值的类
对于类管理的资源,每个对象应该有一份自己的拷贝。
- 定义一个拷贝构造函数,进行资源的拷贝。
- 定义一个析构函数,来释放对象的资源。
- 定义一个拷贝赋值运算符,释放当前资源,并从右侧运算对象拷贝资源。
类值拷贝赋值运算符:赋值运算符通常组合了析构函数和构造函数,一方面需要释放当前资源,另一方面又需要拷贝右侧对象数据。但是这些操作的顺序必须正确,即使是将一个对象赋予本身。
HasPtr& HasPtr::operator=(const HasPtr &rhs){
auto newp = new string(*rhs.ps);//拷贝底层string
delete ps;//释放旧内存
ps = newp;//拷贝数据
i = rhs.i;
return *this;
}
定义行为像指针的类
与值行为类不同,拷贝只拷贝指针本身,但析构时需要注意此时资源有没有被其他对象使用。
于是引入引用计数:
- 除了初始化对象外,每个构造函数(拷贝构造函数除外)还要创建一个引用计数,用来记录有多少对象与正在创建的对象共享状态。
- 拷贝构造函数不分配新的计数器。
- 析构函数递减计数器,如果计数器为0,释放资源。
- 拷贝赋值运算符递增右侧对象的计数器,递减左侧对象的计数器。
定义一个使用引用计数的类:
class HasPtr{
public:
//构造函数分配新的string和新的计数器
HasPtr(const string &s = string())://带有默认参数的构造函数
ps(new 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:
string *ps;
int i;
std::size_t *use;//引用计数
};
HasPtr::~HasPtr(){
if(--*use == 0){
delete ps;//释放string内存
delete use;//释放计数器内存
}
}
HasPtr& HasPtr::operator=(const HasPtr &rhs){
++*rhs.use;
if(--*use == 0){//析构函数的功能
delete ps;
delete use;
}
ps = rhs.ps;
i = rhs.i;
use = rhs.use;
return *this;
}
交换操作
编写我们自己的swap
函数:swap
交换两个对象的成员。
class HasPtr{
friend void swap(HasPtr&, HasPtr&);
...
};
inline void swap(HasPtr &lhs, HasPtr &rhs){//重载标准库的swap
using std::swap;
swap(lhs.ps, rhs.ps);//交换指针
swap(lhs.i, rhs.i);//交换int成员
};
//如果一个类包含一个HasPtr成员
void swap(Foo &lhs, Foo &rhs){
using std::swap;
swap(lhs.h, rhs.h);//使用HasPtr版本的swap,匹配优先级比标准库更高
}
在赋值运算符中使用swap
:通过swap
来定义赋值运算符(拷贝并交换技术)。
HasPtr& HasPtr::operator=(HasPtr rhs){
swap(*this, rhs);//交换rhs和本对象的成员,从而实现赋值
return *this;//rhs在函数结束后被销毁,实际上销毁的是赋值前的本对象
}
拷贝控制示例
以下是代码示例,帮助理解,非常建议阅读一遍。
Message
:存储一条信息,可与多个Folder
关联
save
:储存至某Folder
中remove
:从某Folder
中删除本Message
Folder
:存储多条Message
addMsg
:添加Message
至本Folder
remMsg
:从本Folder
删除某Message
Message
类:
class Message{
friend class Folder;
public:
explicit Message(const string &str = ""):contents(str){}
//拷贝控制成员
Message(const Message&);
Message& operator=(const Message&);
~Message();
//添加/删除本Message
void save(Folder&);
void remove(Folder&);
private:
string contents;
set<Folder*> folders;//包含本Message的Folder
void add_to_Folders(const Message&);
void remove_from_Folders();
};
//向某Folder添加/删除本Message
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::(const Message &m):contents(m.folders){
add_to_Folders(m);
}
//析构函数
void Message::remove_from_Folders(){
for(auto f : folders)
f->remMsg(this);
}
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 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
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);
};
Folder
类:
class Folder{
public:
Folder();
~Folder();
Folder& operator=(const Folder&);
Folder(const Folder&);
void addMsg(Message *m3){
messages.insert(m3);
}
void remMsg(Message *m4){
messages.erase(m4);
}
private:
set<Message*> messages;//保存Message的指针
};
动态内存管理类
以下是一个动态内存管理类的示例,建议阅读一遍加深理解。
StrVec
:Vector
的简化版本,只适用于string
类型
使用allocator
获得原始内存,用construct
成员创建对象,用destroy
销毁元素。
StrVec
有三个指针成员:
elements
,指向分配的内存中的首元素first_free
,指向最后一个实际元素之后的位置cap
,指向分配的内存末尾之后的位置
四个工具函数:
alloc_n_copy
,分配内存,并拷贝给定范围中的元素free
,销毁构造的元素并释放内存chk_n_alloc
,保证StrVec
有足够的空间,如果空间不足,调用reallocate
分配新内存reallocate
,在内存用完时分配新内存
StrVec
类定义:
- 默认构造函数默认初始化
alloc
并将指针初始化为nullptr
- size成员返回当前真正在使用的元素的数目,等于
first_free-elements
- capacity成员返回StrVec可以保存的元素的数目,等于
cap-elements
- 当没有空间容纳新元素,
cap==first_free
,cnk_n_alloc
会重新分配内存 begin
和end
成员分别返回首指针和尾后指针
class StrVec{
public:
StrVec():elements(nullptr), first_free(nullptr), cap(nullptr){}
StrVec(const StvVec&);
StrVec &operator=(const StrVec&);
~StrVec();
void push_back(const string&);
size_t size()const{return first_free - elements;}
size_t capacity()const{return cap - elements;}
string *begin()const{return elements;}
string *end()const{return first_free;}
private:
Static std::allocator<string> alloc;//分配元素
void chk_n_alloc(){//确保空间内存
if(size() == capacity())
reallocate();
}
//工具函数
std::pair<string*, string*> alloc_n_copy
(const string*, const string*);
void free();//释放内存
void reallocate();//重分配内存并拷贝
string* elements;//首元素指针
string* first_free;//内存第一个空闲位置
string* cap;//内存尾后位置
};
void StrVec::push_back(const string& s){
cnk_n_alloc();
alloc.construct(first_free++, s);//构造未初始化的内存
}
pair<string*, string*> Str::alloc_n_copy(const string* b, const string* e){
auto data = alloc.allocate(e - b);
return {data, uninitialized_copy(b, e, data)};
}
void StrVec::free(){
if(elements){
for(auto p = first_free; p != elements;)
alloc.destroy(--p);
alloc.deallocate(elements, cap - elements);
}
}
//分配的空间和元素所需一样多
StrVec::StrVec(const StrVec &s){
auto newdata = alloc_n_copy(s.begin(), s.end());
elements = newdata.first;
first_free = cap = newdata.second;
}
StrVec::~StrVec(){
free();
}
StrVec &StrVec::operator=(const StrVec &rhs){
auto data = alloc_n_copy(rhs.begin(), rhs.end());
free();
elements = data.first;
first_free = cap = data.second;
return *this;
}
移动构造函数和std::move
:避免string
内容的拷贝,移动资源而不是拷贝资源,因此不会调用拷贝构造函数。
void StrVec::reallocate(){
auto newcapacity = size() ? 2*size() : 1;
auto newdata = alloc.allocate(newcapacity);
auto dest = newdata;
auto elem = elements;
for(size_t i = 0; i != size(); ++i)
alloc.construct(dest++, std::move(*elem++));
free();
elements = newdata;
first_free = dest;
cap = elements + newcapacity;
}
对象移动
对于那些通过拷贝完成移动的操作,对象拷贝后就立即被销毁,此时通过移动而非拷贝可以大幅度提升性能。
某些类也不支持拷贝操作,此时只能移动(IO
类、unique_ptr
类)。
右值引用
通过&&
获得右值引用,只能绑定到一个将要销毁的对象,将一个右值引用的资源移动到另一个对象中。
右值引用本质上也只是一个对象的另一个名字。
int i = 1;
int &r = i;
int &&rr = i;//错误,i是一个左值
int &r2 = i*5;//错误,i是一个右值
const int &r3 = i*5;//const引用可以绑定右值
int &&rr2 = i*5;//正确,右值引用绑定右值
左值持久,右值短暂,由于右值只能绑定到临时对象,所以:
- 所引用的对象将要被销毁
- 该对象没有其他用户
变量是左值:
int &&rr1 = 1;//正确,字面常量是右值
int &&rr2 = rr1;//错误,rr1是左值
标准库move
函数(定义在utility
中):虽然不能将右值引用直接绑定到左值上,但是可以显式转换,也可以通过move
获得绑定到左值上的右值引用。
int &&rr3 = std::move(rr1);
移动构造函数和移动赋值运算符
移动构造函数的第一个参数是右值引用,和拷贝构造函数一样,任何额外的参数都必须有默认实参。
资源完成移动,源对象必须不再指向被移动的资源,释放所有权,而移动完成后,源对象会被销毁,调用析构函数。
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
告知标准库该函数不抛出异常,省去一些额外的工作,必须在头文件的声明和定义都指定noexcept
。
在实际中,为了恢复异常导致的任务中断,如vector
在重新分配内存的过程中都必须使用拷贝构造函数,以便在异常中断后仍能恢复至原状态。如果希望进行移动而不是拷贝,则需要显式告诉标准库,我们的移动构造函数可以安全的完成任务。
移动赋值运算符:
StrVec &StrVec::operator=(StrVec &&rhs)noexcept{
//检测自赋值,不能在释放之后调用资源
if(this != &rhs){
free();
elements = rhs.elements;
first_free = rhs.first_free;
cap = rhs.cap;
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}
移后源对象必须可析构:
完成移动操作后,对象的销毁不是保证的,所以需要确保源对象是可析构的,还需保证对象仍是有效的(可以赋予新值),另外,我们的程序不应该依赖源对象中的数据。
在移动操作后,源对象必须保持有效、可析构的状态,但是用户不能对其值进行任何假设(使用)。
合成的移动操作:当一个类没有任何自己版本的拷贝控制成员,且类的每个非static
数据成员都可以移动时,才会合成移动构造函数或移动赋值运算符。
struct X{
int i;;
string s;
};
struct hasX{
X mem;//X有合成的移动操作
};
X x, x2 = std::move(x);//使用合成的移动构造函数
hasX hx, hx = std::move(hx);//使用合成的移动构造函数
合成移动构造函数/移动赋值运算符也可能是删除的:
- 类成员定义了拷贝构造函数但未定义移动构造函数、未定义拷贝构造函数且不能合成移动构造函数
- 类成员的移动构造函数/移动赋值运算符被定义为删除/不可访问
- 析构函数被定义为删除/不可访问
- 类成员有
const
或引用
另外,如果类定义了拷贝构造函数/移动赋值运算符,则类的合成拷贝构造函数和拷贝赋值运算符会被定义为删除的。
移动右值,拷贝左值,如果没有移动构造函数,右值也被拷贝
StrVec v1, v2;;
v1 = v2;//v2是左值,使用拷贝赋值
StrVec getVec(...);
v2 = getVec(...);//getVec返回右值,使用移动赋值
Foo x;//x未定义移动构造函数
Foo y(x);//拷贝构造函数,x是左值
Foo z(std:move(x));//拷贝构造函数,因为为定义移动构造函数
拷贝并交换赋值运算符和移动操作:因为赋值运算符中的参数是非引用的,所以此参数要进行拷贝初始化,因为定义了移动构造函数,左值被拷贝,右值被移动。
class HasPtr{
public:
HasPtr(HasPtr &&p)noexcept : ps(p.ps), i(p.i){
p.ps = 0;
}
HasPtr& operator=(HasPtr rhs){
swap(*this, rhs);
return *this;
}
};
hp = hp2;//hp2是左值,拷贝构造函数
hp = std::move(hp2);//移动构造函数移动hp2
三/五法则:
一个类定义了拷贝操作,它就应该定义所有五个操作(拷贝构造、拷贝赋值、析构、移动构造、移动赋值)。拷贝资源会导致额外开销,在拷贝是非必要的情况下,使用移动可以避免额外开销。
Message
类的移动操作:
void Message::move_Folders(Message *m){
folders = std::move(m->folders);
for(auto f : folders){
f->remMsg(m);
f->addMsg(this);
}
m->folders.clear();//确保销毁m是无害的
}
Message::Message(Message &&m):contents(std::move(m.contents)){
move_Folders(&m);
}
Message& Message::operator=(Message &&rhs){
if(this != &rhs){
remove_from_Folders();;
contents = std::move(rhs,contents);
move_Folders(&rhs);
}
return *this;
}
移动迭代器:一般迭代器的解引用返回指向元素的左值,移动迭代器解引用生成一个右值引用。
调用标准库的make_move_iterator
将普通迭代器转换为一个移动迭代器。
void StrVec::reallocate(){
auto newcapacity = size() ? 2*size() : 1;
auto first = alloc.allocate(newcapacity);
//移动迭代器返回的右值,uninitialized_copy的construct会调用移动构造函数来构造元素。
auto last = uninitialized_copy(make_move_iterator(begin())),
make_move_iterator(end()),
first);
free();
elements = first;
first_free = last;
cap = elements + newcapacity;
}
不要随意使用移动操作:
移动之后,源对象的状态是不确定的,必须保证源对象没有其他用户。
右值引用和成员函数
void push_back(const string& s){//左值引用,拷贝
cnk_n_alloc();
alloc.construct(first_free++, s);
}
void push_back(string&& s){//非const右值引用,移动
cnk_n_alloc();
alloc.construct(first_free++, std::move(s));
}
string s = "some";
vec.push_back(s);//调用const string&
vec.push_back("ss");//调用string&&
右值和左值引用成员函数:有时向右值赋值是合法的,为了阻止此操作,可以使用引用限定符
//向右值赋值
s1 + s2 = "wow";
//引用限定符&和&&分别指出this可以指向一个左值或右值
class Foo{
public:
Foo &operator=(const Foo&) &;//只能向可修改的左值赋值
};
Foo& retFoo();//返回引用,左值
Foo retVal();//返回值,右值
Foo i, j;
retFoo() = j;//正确,返回左值
retVal() = j;//错误,返回右值
重载和引用函数:
class Foo{
public:
Foo sorted() &&;//可用于可改变的右值
Foo sorted() const &;;//可用于任何类型的Foo
private:
vector<int> data;
};
//本对象是右值,可以在原址进行排序
Foo Foo::sorted() &&{
sort(data.begin(), data.end());;;
return *this;
}
//本对象是const或左值,不能对原址进行排序
Foo Foo::sorted() const &{
Foo ret(*this);//拷贝副本
sort(ret.data.begin(), ret.data.end());
return ret;
}
//调用两个函数会根据调用对象是左值还是右值来判断
retVal().sorted();//右值,在原址排序
retFoo().sorted();//左值,在拷贝上排序