《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. 构造函数要创建计数,用来记录有多少对象和正在创建的对象共享状态。创建对象时,只有一个对象共享状态,计数器初始化为1。
    2. copy构造函数不分配新的计数器,而是copy指定对象的数据成员,包括计数器。拷贝构造函数递增共享的计数器,指出给定对象的状态又被一个新用户所共享。
    3. 析构函数递减计数器,指出共享状态的用户又少了一个。如果计数器变为0,则析构函数delete掉内存。
    4. 拷贝赋值运算符递增右侧对象的计数器,递减左侧运算对象的计数器。如果(原)左侧对象的计数器变为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++学习不能落下,且随疾风前行。

时间: 2024-10-25 12:08:04

《C++ Primer》读书笔记—第十三章 控制拷贝的相关文章

Java编程思想第四版读书笔记——第十三章 字符串

Java编程思想第四版读书笔记--第十三章 字符串 字符串的操作是计算机程序设计中最常见的行为. 关键词: StringBuilder ,StringBuffer,toString(),format转换,正则表达式, 1.不可变String String对象时不可变的.每当把String对象作为方法的参数时,都会复制一份引用.(其实就是对函数中参数列表中参数的操作不会影响外面的原参数) 如下: import static net.mindview.util.Print.*; public cla

《APUE》读书笔记第十三章-守护进程

守护进程 守护进程是生存期较长的一种进程,它们常常在系统自举时启动,仅在系统关闭时才终止.因为它们没有控制终端,所以说它们是在后台运行的.UNIX系统由很多守护进程,它们执行日常事务活动. 本章主要介绍守护进程的结构,以及如何编写守护进程程序和守护进程如何报告错误情况. 一.守护进程的编程规则 (1)首先要做的是调用umask将文件模式创建屏蔽字设置为0.这是由于继承得来的文件模式创建屏蔽字可能会拒绝设置某些权限. (2)调用fork,然后使父进程退出(exit). (3)调用setsid以创建

《C++ Primer 4th》读书笔记 第4章-数组和指针

原创文章,转载请注明出处: http://www.cnblogs.com/DayByDay/p/3911573.html <C++ Primer 4th>读书笔记 第4章-数组和指针

《C++ Primer 4th》读书笔记 第5章-表达式

原创文章,转载请注明出处: http://www.cnblogs.com/DayByDay/p/3912114.html <C++ Primer 4th>读书笔记 第5章-表达式

《C++ Primer 4th》读书笔记 第7章-函数

原创文章,转载请注明出处:http://www.cnblogs.com/DayByDay/p/3912413.html <C++ Primer 4th>读书笔记 第7章-函数

《C++ Primer 4th》读书笔记 第6章-语句

原创文章,转载请注明出处: http://www.cnblogs.com/DayByDay/p/3912407.html <C++ Primer 4th>读书笔记 第6章-语句

C++primer读书笔记11-多态

多态也是C++中的一个重要的方面,多态和动态类型,虚函数本质上是指相同的事情. 1 虚函数 类中的成员函数原型前面加上virtual 表面这个函数是个虚函数.虚函数的目的是为了在继承它的派生类中重新定义这个函数,以便于通过基类的指针或引用在运行时对派生类的函数进行调用. 2 派生类和虚函数 派生类一般情况下要重定义所继承的虚函数,有几个注意事项. <1>虚函数的声明必须和基类中的函数声明原型完全一致,例外的是当基类返回基类型的指针或者引用的时候,派生类可以派生类类型的指针或者引用 <2&

C++中的volatile(Primer读书笔记)

时间:2014.05.24 地点:基地 -------------------------------------------------------------------------- 一.简述 volatile限定符平时很少用到,今天倒是碰到了,所幸探个明白.volatile 英文字面意思是"不稳定的",确切的计算机含义时与机器相关,所以在对包含volatile的程序在移植到新机器或使用不同的编译器时往往还需要对编译器进行一些改变. -----------------------

C++ primer读书笔记10-继承

封装,继承,多态是C++的三大基本概念,这里着重总结一下继承相关的东西 1 类派生列表 类派生列表指定派生类要继承的基类,派生列表中有一个或者多个基类如: class B : public A1,protected A2,private A3 但是单继承时最常见的,多继承不多见 2 派生类的定义 派生类继承时,会包含父类的所有成员,即便私有成员不能被访问.父类中的虚函数,在派生类中一般也要定义,如 果不定义的话,派生类将继承基类的虚函数 3 基类必须是已经定义的 一个仅仅声明的类,是不能出现在派