首页 > 代码库 > 《C++ Primer》读书笔记—第十三章 控制拷贝
《C++ Primer》读书笔记—第十三章 控制拷贝
声明:
- 文中内容收集整理自《C++ Primer 中文版 (第5版)》,版权归原书所有。
- 学习一门程序设计语言最好的方法就是练习编程
第III部分,类设计者的工具
1、类是C++的核心概念。每个类都定义了一个新类型和在此类型对象上可执行的操作。
2、当定义一个类时,我们显式或隐式地指定在此类型的对象的拷贝、移动、赋值和销毁时做什么。一个类通过定义五种特殊的成员函数来控制这些操作,包括:拷贝构造函数(copy construcor),拷贝赋值运算符(copy-assignment operator)、移动构造函数(move constructor)、移动赋值运算符(move-assignment operator)和析构函数(destructor)。
一、拷贝、赋值与销毁
1、如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。拷贝构造函数第一个参数必须是一个引用类型。
2、如果我们没有给一个类定义拷贝构造函数,则编译器会定义一个合成拷贝构造函数,会将其参数的成员逐个拷贝到正在创建的对象中,编译器从给定对象中依次将每个非static成员拷贝到正在创建的对象中。
对于类类型,会使用其拷贝构造函数来拷贝,内置类型的成员则直接拷贝。如果数组成员是类类型,则使用元素的拷贝构造函数来进行拷贝。
拷贝构造函数通常不应该是explicit的,因为在几种情况下都会被隐式地使用。
3、直接初始化与复制初始化的不同:直接初始化直接调用与实参匹配的构造函数,复制初始化总是调用复制构造函数。复制初始化首先使用指定构造函数创建一个临时对象,然后用复制构造函数将那个临时对象复制到正在创建的对象。所以当复制构造函数被声明为私有时,所有的复制初始化都不能使用。
1 string dots(10, ‘.‘); //直接初始化 2 string s(dots); //直接初始化 3 string s2 = dots; //拷贝初始化 4 string null_book = "9-999-99999-9"; //拷贝初始化 5 string nines = string(100, ‘9‘); //拷贝初始化
4、如果一个类有移动构造函数,则拷贝初始化有时会使用移动构造函数而非拷贝构造函数来完成。
5、直接初始化时,我们要求编译器使用普通的函数匹配,来选择参数最匹配的构造函数。使用构造初始化时,我们要求编译器将右侧运算对象拷贝到正在创建的对象中(还可能要类型转换)。
拷贝初始化首先使用指定构造函数创建一个临时对象,然后后用拷贝构造函数将那个临时对象拷贝到正在创建的对象。
传递非引用实参,函数返回非引用类型,花括号列表初始化数组或聚合类时都用到拷贝构造函数。
复制构造函数 不等于 operator=(), 后者是赋值运算符。
拷贝初始化首先使用构造函数创建一个临时对象,然后用复制构造函数将那个临时对象复制到正在创建的对象。
拷贝构造函数被用来初始化非引用类类型参数,这一特性揭示了为什么拷贝构造函数自己的参数必须是引用类型,如果其参数不是引用类型,则调用永远不会成功(参考不完全类型)。为了调用拷贝构造函数,我们必须拷贝它的实参,但为了拷贝实参,我们有需要调用拷贝构造函数,如此无限循环(递归)。
1 vector<int> v1(10); //正确:直接初始化 2 vector<int> v2 = 10;//错误,接受大小参数的vector构造函数是explicit的 3 void f(vector<int> v); 4 f(10); //错误,原因同上 5 f(vector<int>(10)); //正确,拷贝构造函数
6、编译器可以略过拷贝构造函数。(不是必须)。在这个程序点上,拷贝/移动构造函数必须是存在且可访问的。(比如说不能是private的)。
7、拷贝赋值运算符:operator。重载运算符本质是函数,参数表示运算符的运算对象。赋值运算符通常返回一个指向其左侧运算对象的引用。标准库要求保存在容器中的类型要具有赋值运算符,且其返回值是左侧运算对象的引用。合成拷贝赋值运算符被用来禁止该类型对象的赋值。
下列代码等价于Sales_data的合成拷贝赋值运算符:
1 Sales_data& Sale_sdata::operator=(const Sales_data &rhs) 2 { 3 bookNo = rhs.bookNo;//调用string::operator=(); 4 units_sold = rhs.units_sold; //使用内置的int赋值 5 revenue = rhs.revenue;//同上 6 return *this;//返回引用是习惯做法 7 8 }
8、析构函数:
析构函数初始化对象的非static数据成员。析构函数释放对象使用的资源,并销毁对象的非static数据成员。名字由波浪号接类名构成。由于析构函数不接受参数,因此不能被重载。
在构造函数中,成员初始化是在函数体执行之前完成的,并按照他们在类中出现的顺序进行初始化(使用初始化列表才是初始化,函数体中是给成员赋值)。
而在析构函数中,首先执行函数体,然后销毁成员。成员按照初始化的顺序逆序销毁。
通常,析构函数释放对象在生存期分配的所有资源。static不是在某个对象的生存期分配的,是贯穿程序的。
内置类型没有析构函数,销毁内置类型什么都不用做,销毁类类型时,调用其析构函数。
什么时候调用析构函数:
1 变量离开其作用域是被销毁
2 让一个对象被销毁时,其成员被销毁(比如类里面还有一个类,大类对象被销毁时,小类对象也被销毁)
3 容器(无论是标准库容器还是数组被销毁时,其元素被销毁)
4 动态分配内存的对象,当对指向它的指针应用delete运算符时被销毁
5 对于临时对象,当创建它的完整表达式结束时被销毁。A a = 1; 这里面就有一个临时对象。
当指向一个对象的引用或指针离开作用域时,析构函数不会执行。
析构函数体自身并不直接销毁成员,成员是在析构函数体之后隐含的析构阶段被销毁的,在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分进行的。
(先执行函数体,然后再析构)
9、三/五法则:
有三个基本操作可以控制类的拷贝操作:拷贝构造函数、拷贝赋值运算符、析构函数。通常他们作为一个整体出现,只需要一个操作而不需要其他操作的情况是很少见的。
要析构 =》 要delete =》要new =》不能直接copy或直接给指针赋值需要copy构造函数和operator=)。
当决定一个类是否要定义它自己版本的拷贝控制成员时,一个基本的原则是首先确定这个类是否需要一个析构函数。如果一个类需要一个析构函数,则一定需要拷贝构造函数和拷贝赋值运算符。
如果对象里有指针,但使用了合成的copy构造函数和operator=(), 拷贝的时候两个对象里的指针都指向同一个内存,当其中一个对象被销毁的时候,指针会被delete,导致另一个对象的指针指向了一块被释放掉的内存。shared_pointer 可破之。
有析构函数 ====几乎一定需要====》 有拷贝构造函数和拷贝赋值运算符
有拷贝构造函数====几乎一定需要====》拷贝赋值运算符
有拷贝构造函数和拷贝赋值运算符 =====不一定需要====》析构函数
10、使用=default:合成的函数将隐式地声明为内联的。如果不希望合成成员是内联函数,则只对成员的类外定义使用=default。
1 class Sales_data 2 { 3 Sales_data() = default; 4 Sales_data(const Sales_data&) = default; 5 Sales_data& operator=(const Sales_data&); 6 ~Saled_data() = default; 7 }; 8 Sales_data: Sales_data::operator=(const Sales_data&) = default;
11、大多数类应该定义默认构造函数、拷贝构造函数、和拷贝赋值运算符,无论是隐式地还是显式地。
iostream类阻止了拷贝,以避免多个对象写入或读取相同的IO缓冲。
为了阻止拷贝,我们可以定义删除的函数(=delete)。
1 struct NoCopy 2 { 3 NoCopy() = default; //使用合成的默认构造函数 4 NoCopy(const NoCopy&) = delete; //阻止拷贝 5 NoCopy &operator=(const NoCopy&) = delete; //阻止赋值 6 ~NoCopy() = default; //使用合成的析构函数 7 };
声明但并不实现:
我们也可以将这些拷贝控制成员声明为private的,但并不定义他们。声明但并不定义一个成员函数是合法的。如果单单声明为private,友元和其他成员函数还是可以用。
试图拷贝对象的用户代码将在编译阶段报错,成员函数或友元函数中的拷贝操作将在链接时报错。
二、拷贝控制和资源管理
1、当定义一个类的拷贝构造函数和赋值操作运算符时,我们要确定要让这个类的的行为向一个值还是像一个指针。
行为像一个值:有自己的状态,拷贝时,副本和原对象是相互独立的,改变副本不会影响原对象。
行为像一个指针:意味着状态是共享的,当我们拷贝这种类的对象时,副本和原对象使用相同的底层数据,改变副本也会改变原对象。反之亦然。
标准库容器和string类的行为像一个值,而不出意外,share_ptr类提供 类似指针的行为。IO类型和unique_ptr不允许拷贝和赋值,因此他们的行为既不像值也不像指针。
通常:类直接拷贝内置类型(不包括指针)成员:这些成员本身就是值,因此通常应该让他们的行为像值一样。我们如何拷贝指针成员决定了类具有类值行为还是类指针行为。
类(似)值版本的HasPtr如下所示:
1 class HasPtr 2 { 3 public: 4 HasPtr(const std::string &s = std::string()): 5 ps(new std::string(s)), i(0) { }; 6 HasPtr(const HasPtr &p): 7 ps(new std::string(*p.ps)), i(p.i) { }; //注意new了一个新指针,并用p.ps指向的值初始化,然后两者再无关联了 8 HasPtr& operator=(const HasPtr &); 9 ~HasPtr() {delete ps;} 10 private: 11 std::string *ps; 12 int i; 13 };
这个构造函数动态分配他自己的string副本,并将指向string的指针保存在ps中。拷贝构造函数也分配他自己的string副本。
类值拷贝赋值运算符:
赋值运算符通常组合了析构函数和构造函数的操作。类似拷贝构造函数,赋值操作会销毁左侧运算对象的资源。类似拷贝构造函数,赋值操作会从右侧对象拷贝数据。但是我们要注意自身赋值给自身的这种情况。
1 HasPtr& HasPtr::operator=(const HasPtr &rhs) 2 { 3 auto newp = new std::string(*rhs.ps); //new并拷贝string 4 delete ps; 5 ps = newp; 6 i = rhs.i; 7 //delete newp; //这里不能delete,否则ps指向的内存会被释放掉 8 return *this; 9 }
赋值操作时,先用【右】操作数的指针指向的值初始化【左】操作数的string(这里要用一个临时变量newp),然后再释放掉【左】操作数原来指向的内存,最后【左】操作数的指针赋为newp的值。最后复制一些内置类型并返回*this。
对一个赋值运算符来说,一个好的方法是在销毁左侧运算对象资源之前拷贝右侧运算对象。
2、对于行为类似像指针的类,我们需要为其定义拷贝构造函数和拷贝赋值运算符,来拷贝指针成员本身而不是它指向的string。我们的类仍然需要自己的析构函数来释放接受string参数的构造函数分配的内存。本例中,只有当最后一个指向string的HasPtr销毁时,它才可以释放string
3、引用计数:
计算被某内存被指针引用的次数
- 构造函数要创建计数,用来记录有多少对象和正在创建的对象共享状态。创建对象时,只有一个对象共享状态,计数器初始化为1。
- copy构造函数不分配新的计数器,而是copy指定对象的数据成员,包括计数器。拷贝构造函数递增共享的计数器,指出给定对象的状态又被一个新用户所共享。
- 析构函数递减计数器,指出共享状态的用户又少了一个。如果计数器变为0,则析构函数delete掉内存。
- 拷贝赋值运算符递增右侧对象的计数器,递减左侧运算对象的计数器。如果(原)左侧对象的计数器变为0,参照3。
1 class HasPtr 2 { 3 public: 4 HasPtr(const std::string& s = std::string()): 5 ps(new std::string(s)), i(0), use(new std::size_t(1)) {}; //计数器置为1 6 7 HasPtr(const HasPtr &p): 8 ps(p.ps), i(p.i), use(p.use) {++ *use;} //递增计数器 9 HasPtr& operator=(const HasPtr&); 10 ~HasPtr(); 11 private: 12 std::string *ps; 13 int i; 14 std::size_t *use; //记录有多少和对象共享*ps的成员。 15 };
如果计数器变成0,则析构函数释放ps和use指向的内存。
1 HasPtr::~HasPtr() 2 { 3 if(--*use == 0) //递减计数器 4 { 5 delete ps; //释放string内存 6 delete use; //释放计数器内存 7 } 8 }
4、实现operator=时,应该先递增右操作数的引用次数,在递减左操作数的引用计数(否则应对自赋值这种情况会有麻烦)
1 HasPtr& HasPtr::operator=(const HasPtr &rhs) 2 { 3 ++*rhs.use; 4 if(--*use == 0)//如果这步放在++*rhs.use;的右边的话,自赋值就会把自身(本不应被delete的内存delete掉了) 5 { 6 delete ps; 7 delete use; 8 } 9 ps = rhs.ps; 10 use = rhs.use; 11 i = rhs.i; 12 return *this; 13 }
三、交换操作
1、为了交换两个对象我们需要进行一次拷贝和两次赋值
1 HasPtr temp = v1; 2 v1 = v2; 3 v2 = temp;
我们更希望交换指针,而不用分配string的新副本:
1 string *temp = v1.ps; 2 v1.ps = v2.ps; 3 v2.ps = temp;
2、编写自己的swap函数:
swap就是为了优化代码,所以声明为inline,在swap内部逐个交换内置类型,所以使用std::swap.
swap并不是必要的,但是对于分配了资源的类,定义swap可能是一种很重要的优化手段。
1 class HasPtr 2 { 3 friend void swap(HasPtr&, HasPtr&); 4 } 5 inline void swap(HasPtr &lhs, HasPtr &rhs) 6 { 7 using std::swap; 8 swap(lhs.ps, rhs.ps); 9 swap(lhs.i, rhs.i); 10 }
3、在赋值运算符中使用swap,使用了一种名为【拷贝并交换】的技术,将左侧运算对象的一个对象和右侧运算对象的一个对象进行交换。
1 HasPtr& HasPtr::operator=(HasPtr rhs) 2 { 3 swap(*this, rhs); 4 5 //rhs里的指针指向了*this里面指针原来指向的位置,函数结束之后自动销毁 6 return *this; 7 }
四、拷贝控制示例
1、定义两个类,用于邮件处理应用。Message和Folder,代表邮件消息和消息目录。
为了记录每个Message位于哪些Folder中,每个Message保存一个它所在的Folder的指针的set。同样,每个Folder也保存一个它包含的Message的指针的set。
Message提供save和remove操作,向folder添加和删除一个message。
Message类:
1 class Message 2 { 3 friend class Folder; 4 public: 5 //folders被隐式初始化为空集合 6 explicit Message(const std::string &str = ""); 7 Message(const Message&); 8 ~Message(); 9 //从给定的Folder中添加/删除本Message 10 void save(Folder&); 11 void remove(Folder&); 12 private: 13 std::string contents; //包含消息文本 14 //folders被隐式初始化为空集合 15 std::set<Folder*> folders; //包含本Message的Folder 16 //拷贝构造函数、拷贝赋值运算符和析构函数所使用的工具函数 17 //将本Message添加到指定参数的Folder中 18 void add_to_Folders(const Message&); 19 //从folders中的每个Folder中删除本Message 20 void remove_from_Folders(); 21 22 };
Save和Remove类:
1 void Message::save(Folder &f) 2 { 3 //在Message中添加Folder的指针,然后在Folder中添加Message的指针 4 5 folders.insert(&f); //将给定Folder的指针添加到我们的Folder列表中 6 f.addMsg(this); 7 } 8 void Message::remove(Folder &f) 9 { 10 folders.erase(&f); //对应上面的insert 11 f.remMsg(this); //对应上面的addMsg 12 }
Message类的析构函数:
1 void Message::remove_from_Folders()//析构和赋值都会用到这个工具函数 2 { for(auto i : folders) 3 folders.remMsg(this); 4 } 5 6 Message::~Message() 7 { 8 remove_from_Folders(); 9 }
Message类的拷贝构造函数:
1 </pre><pre name="code" class="cpp">Message::Message(const Message &m):contest(m.contest),folders(m.folders) 2 { 3 /* 4 for(auto f : m.folders) //也可以,不过使用工具函数更方便 5 f->addMsg(this); 6 */ 7 //使用上一个工具函数 8 add_to_Folders(m); 9 }
Message的拷贝赋值运算符:
1 Message& operator=(const Message& rhs) 2 { 3 remove_from_Folders(); 4 folders = rhs.folders; 5 add_to_Folders(rhs); 6 contents = rhs.contents; 7 return *this; 8 }
Swap类:
1 void swap(const Message& lhs, const Message& rhs) 2 { 3 //将每个Msg从原来的Folder中删除 4 for(auto f : lhs.folder) 5 f.remMsg(&lhs); 6 for(auto f : rhs.folder) 7 f.remMsg(&rhs); 8 //交换Folders 9 using std::swap; 10 swap(lhs.content, rhs.content); //使用swap(string&, string&); 11 swap(lhs.folders, rhs.folders); //使用swap(set&, set&); 12 //将每个Msg添加到新的Folder中。 13 for(auto f : lhs.folder) 14 f.addMsg(&lhs); 15 for(auto f : rhs.folder) 16 f.addMsg(&rhs); 17 }
五、动态内存管理类???
1、
2、
3、
六、对象移动???
1、
2、
3、
4.1:第十三章第五、六章还需要回来再看。马上要小长假了,今天把第十三章笔记总结了下。假期估计要把“排行榜”的项目做完,收假了要开始看师兄给的论文了。C++学习不能落下,且随疾风前行。
《C++ Primer》读书笔记—第十三章 控制拷贝