拷贝控制2(拷贝控制和资源管理/交换操作)

为了定义拷贝构造函数和拷贝赋值运算符,我们首先必须确认此类型对象的拷贝语义。通常可以定义拷贝操作,使类的行为看起来像一个值或者像一个指针(即所谓的深拷贝和浅拷贝)

类的行为像一个值,意味着它应该也有自己的状态。当我们拷贝一个像值的对象时,副本和原对象是完全独立的。改变副本不会对原对象有任何影响,反之亦然

行为像指针的类则共享状态。当我们拷贝一个这种类的对象时,副本和原对象使用相同的底层数据。改变副本也会改变原对象,反之亦然

在我们使用过的标准库类中,标准库容器和 string 类的行为像一个值。shared_ptr 提供类似指针的行为。IO 类型和 unique_ptr 不允许拷贝或赋值,因此它们的行为既不像值也不像指针

行为像值的类:

 1 #include <iostream>
 2 using namespace std;
 3
 4 class HasPtr{
 5 public:
 6     HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0) {}
 7     HasPtr(const HasPtr &p) : ps(new std::string(*p.ps)), i(p.i) {}
 8     HasPtr& operator=(const HasPtr&);
 9     ~HasPtr(){
10         delete ps;
11     }
12
13     ostream& print(ostream &os){
14         os << i << " " << *ps << " " << ps;
15         return os;
16     }
17
18 private:
19     std::string *ps;
20     int i;
21 };
22
23 HasPtr& HasPtr::operator=(const HasPtr &rhs){
24     auto newp = new string(*rhs.ps);//为了避免两个对象中ps指针相同时出错先将rhs.ps中的内容存放到一个新开辟的空间newp中
25     delete ps;//释放旧内存
26     ps = newp;
27     i = rhs.i;
28     return *this;
29 }
30
31 int main(void){
32     HasPtr s1("hello");
33     HasPtr s2 = s1;
34     HasPtr s3;
35     s3 = s1;
36
37     s1.print(cout) << endl;
38     s2.print(cout) << endl;
39     s3.print(cout) << endl;
40
41 // 输出:
42 //     0 hello 0x2a811d8
43 //     0 hello 0x2a811a8
44 //     0 hello 0x2a81198
45
46     return 0;
47 }

注意:在赋值运算符应该要防范自赋值的情况:

1 HasPtr& HasPtr::operator=(const HasPtr &rhs){
2     delete ps;//如果rhs和本对象是同一个对象,则rhs.ps将成为一个空悬指针
3     ps = new string(*rhs.ps);//错误,我们试图解引用一个空悬指针
4     i = rhs.i;
5     return *this;
6 }

行为像指针的类:

 1 #include <iostream>
 2 using namespace std;
 3
 4 class HasPtr{
 5 public:
 6     HasPtr(const std::string &s = std::string()) :
 7         ps(new std::string(s)), i(0), use(new std::size_t(1)) {}//直接初始化时引用计数为1
 8     HasPtr(const HasPtr &p) : ps(p.ps), i(p.i), use(p.use) {
 9         ++*use;//引用计数加一
10     }
11     HasPtr& operator=(const HasPtr&);
12     ~HasPtr();
13
14     ostream& print(ostream &os){
15         os << *use << " " << i << " " << *ps << " " << ps;
16         return os;
17     }
18
19 private:
20     std::string *ps;
21     int i;
22     std::size_t *use;//记录当前有多少个对象共享*ps成员
23 };
24
25 HasPtr::~HasPtr() {
26     if(--*use == 0){//每析构一个HasPtr对象引用计数减一
27         delete ps;//如果引用计数为0,释放ps和use所指的内存
28         delete use;
29     }
30 }
31
32 HasPtr& HasPtr::operator=(const HasPtr &rhs){
33     ++*rhs.use;//递增右侧运算对象的引用计数
34     if(--*use == 0){//然后递减本对象的引用计数
35         delete ps;//如果没有其它用户
36         delete use;//释放本对象分配的内存
37     }
38     ps = rhs.ps;
39     i = rhs.i;
40     use = rhs.use;
41     return *this;
42 }
43
44 int main(void){
45     HasPtr s1("hello");
46     HasPtr s2 = s1;
47     HasPtr s3;
48     s3 = s1;
49     HasPtr s4("word");
50
51     s1.print(cout) << endl;
52     s2.print(cout) << endl;
53     s3.print(cout) << endl;
54     s4.print(cout) << endl;
55
56 // 输出:
57 // 3 0 hello 0x2d31218
58 // 3 0 hello 0x2d31218
59 // 3 0 hello 0x2d31218
60 // 1 0 word 0x2b610c8
61
62     return 0;
63 }

注意:为了实现类似于 shared_ptr 的引用计数功能,我们可以将计数器保持到动态内存中,指向相同 ps 对象的 HasPtr 也指向相同的 use 对象。 这里我们不能使用 static 来实现引用计数,因为它是属于类本身的,这意味着所有 HasPtr 类的对象中 use 值都是相等的,并且我们将无法做到给赋值运算符右侧对象 use 加一,左侧对象 use 减一:

 1 #include <iostream>
 2 using namespace std;
 3
 4 class HasPtr{
 5 public:
 6     HasPtr(const std::string &s = std::string()) :
 7         ps(new std::string(s)), i(0) {}//直接初始化时引用计数为1
 8     HasPtr(const HasPtr &p) : ps(p.ps), i(p.i) {
 9         ++use;//引用计数加一
10     }
11     HasPtr& operator=(const HasPtr&);
12     ~HasPtr();
13
14     ostream& print(ostream &os){
15         os << use << " " << i << " " << *ps << " " << ps;
16         return os;
17     }
18
19 private:
20     std::string *ps;
21     int i;
22     static std::size_t use;//记录当前有多少个对象共享*ps成员
23 };
24
25 HasPtr::~HasPtr() {
26     if(--use == 0){//每析构一个HasPtr对象引用计数减一
27         delete ps;//如果引用计数为0,释放ps所指的内存
28     }
29 }
30
31 HasPtr& HasPtr::operator=(const HasPtr &rhs){
32     ++rhs.use;//递增右侧运算对象的引用计数
33     if(--use == 0){//然后递减本对象的引用计数
34         delete ps;//如果没有其它用户,释放本对象分配的内存
35     }
36     ps = rhs.ps;
37     i = rhs.i;
38     use = rhs.use;
39     return *this;
40 }
41
42 size_t HasPtr::use = 1;
43
44 int main(void){
45     HasPtr s1("hello");
46     HasPtr s2 = s1;
47     HasPtr s3;
48     s3 = s1;
49     HasPtr s4("word");
50
51     s1.print(cout) << endl;
52     s2.print(cout) << endl;
53     s3.print(cout) << endl;
54     s4.print(cout) << endl;
55
56 // 输出:
57 // 2 0 hello 0x2be1248
58 // 2 0 hello 0x2be1248
59 // 2 0 hello 0x2be1248
60 // 2 0 word 0x2be10b8
61
62     return 0;
63 }

交换操作:

库函数 swap 的实现依赖于类的拷贝构造函数和赋值运算符。如,对值语义的 HasPtr 类对象使用库函数 swap:

 1 #include <iostream>
 2 using namespace std;
 3
 4 class HasPtr{
 5 public:
 6     HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0) {}
 7     HasPtr(const HasPtr &p) : ps(new std::string(*p.ps)), i(p.i) {}
 8     HasPtr& operator=(const HasPtr&);
 9     ~HasPtr(){
10         delete ps;
11     }
12
13     ostream& print(ostream &os){
14         os << i << " " << *ps << " " << ps;
15         return os;
16     }
17
18 private:
19     std::string *ps;
20     int i;
21 };
22
23 HasPtr& HasPtr::operator=(const HasPtr &rhs){
24     auto newp = new string(*rhs.ps);//为了避免两个对象中ps指针相同时出错先将rhs.ps中的内容存放到一个新开辟的空间newp中
25     delete ps;//释放旧内存
26     ps = newp;
27     i = rhs.i;
28     return *this;
29 }
30
31 int main(void){
32     HasPtr s1("hello");
33     HasPtr s2("word");
34
35     s1.print(cout) << endl;
36     s2.print(cout) << endl;
37
38     swap(s1, s2);
39     // HasPtr cmp = s1;
40     // s1 = s2;
41     // s2 = cmp;
42
43     s1.print(cout) << endl;
44     s2.print(cout) << endl;
45
46 // 输出:
47 // 0 hello 0x28411c8
48 // 0 word 0x2841238
49 // 0 word 0x28410d8
50 // 0 hello 0x28410e8
51
52     return 0;
53 }

显然,swap(s1, s2);的实现流程是:

HasPtr cmp = s1;
s1 = s2;
s2 = cmp;

这个过程中分配了 3 次内存,效率及其低下。理论上这些内存分配都是不必要的。我们可以只交换指针而不需要分配 string 的新副本。

因此,除了定义拷贝控制成员,管理资源的类通常还需要定义一个名为 swap 的函数。尤其对于那些与重排元素顺序的算法一起使用的类,定义 swap 是非常重要的。这类算法在需要交换两个元素时会调用 swap。

给值语义的 HasPtr 编写 swap 函数:

 1 #include <iostream>
 2 using namespace std;
 3
 4 class HasPtr{
 5 friend void swap(HasPtr&, HasPtr&);
 6
 7 public:
 8     HasPtr(const std::string &s = std::string(), int a = 0) : ps(new std::string(s)), i(a) {}
 9     HasPtr(const HasPtr &p) : ps(new std::string(*p.ps)), i(p.i) {}
10     HasPtr& operator=(const HasPtr&);
11     ~HasPtr(){
12         delete ps;
13     }
14
15     ostream& print(ostream &os){
16         os << i << " " << *ps << " " << ps;
17         return os;
18     }
19
20 private:
21     std::string *ps;
22     int i;
23 };
24
25 HasPtr& HasPtr::operator=(const HasPtr &rhs){
26     auto newp = new string(*rhs.ps);//为了避免两个对象中ps指针相同时出错先将rhs.ps中的内容存放到一个新开辟的空间newp中
27     delete ps;//释放旧内存
28     ps = newp;
29     i = rhs.i;
30     return *this;
31 }
32
33 inline
34 void swap(HasPtr &lhs, HasPtr &rhs){
35     swap(lhs.ps, rhs.ps);
36     swap(lhs.i, rhs.i);
37 }
38
39 int main(void){
40     HasPtr s1("hello");
41     HasPtr s2("word", 1);
42
43     s1.print(cout) << endl;
44     s2.print(cout) << endl;
45
46     swap(s1, s2);
47     // auto cmp = s1.ps;
48     // s1.ps = s2.ps;
49     // s2.ps = cmp;
50     // auto cnt = s1.i;
51     // s1.i = s2.i;
52     // s2.i = cnt;
53
54     s1.print(cout) << endl;
55     s2.print(cout) << endl;
56
57 // 输出:
58 // 0 hello 0x2bb1208
59 // 1 word 0x2bb1128
60 // 1 word 0x2bb1128
61 // 0 hello 0x2bb1208
62
63     return 0;
64 }

注意:如果存在类型特定的 swap 版本,其匹配程度会优于 std 中定义的版本。如果不存在类型特定的版本,则会使用 std 中的版本(假定作用域中有 using 声明)

类指针的 HasPtr 版本并不能从 swap 函数受益

在赋值运算符中使用 swap:

定义了 swap 的类中通常用 swap 来定义它们的赋值运算符。这些运算符使用了一种名为 拷贝并交换(copy and swap) 的技术。这种技术将左侧运算对象与右侧运算对象的一个副本进行交换:

 1 #include <iostream>
 2 using namespace std;
 3
 4 class HasPtr{
 5 friend void swap(HasPtr&, HasPtr&);
 6
 7 public:
 8     HasPtr(const std::string &s = std::string(), int a = 0) : ps(new std::string(s)), i(a) {}
 9     HasPtr(const HasPtr &p) : ps(new std::string(*p.ps)), i(p.i) {}
10     HasPtr& operator=(HasPtr);
11     ~HasPtr(){
12         delete ps;
13     }
14
15     ostream& print(ostream &os){
16         os << i << " " << *ps << " " << ps;
17         return os;
18     }
19
20 private:
21     std::string *ps;
22     int i;
23 };
24
25 HasPtr& HasPtr::operator=(HasPtr rhs){//注意这里不能是引用
26     swap(*this, rhs);//交换后rhs指向本对象曾经使用的内存
27     return *this;//作用域结束,rhs被销毁,从而delete了rhs种的指针
28 }
29
30 inline
31 void swap(HasPtr &lhs, HasPtr &rhs){
32     swap(lhs.ps, rhs.ps);
33     swap(lhs.i, rhs.i);
34 }
35
36 int main(void){
37     HasPtr s1("hello");
38     HasPtr s2("word", 1);
39
40     s1.print(cout) << endl;
41     s2.print(cout) << endl;
42
43     swap(s1, s2);
44
45     s1.print(cout) << endl;
46     s2.print(cout) << endl;
47
48 // 输出:
49 // 0 hello 0x2ef1128
50 // 1 word 0x2ef1088
51 // 1 word 0x2ef1088
52 // 0 hello 0x2ef1128
53
54     return 0;
55 }

注意:这个版本赋值运算符中,参数并不能是引用

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

原文地址:https://www.cnblogs.com/geloutingyu/p/8418835.html

时间: 2024-08-14 22:21:15

拷贝控制2(拷贝控制和资源管理/交换操作)的相关文章

C++复制控制:拷贝构造函数

一.拷贝构造函数是一种特殊构造函数,具有单个形参,该形参(常用const修饰)是对该类类型的引用.与默认构造函数一样 ,拷贝构造函数可由编译器隐式调用.拷贝构造函数应用的场合为: (1)根据另一个同类型的对象显式或隐式初始化一个对象. (2)复制一个对象将它作为实参传给一个函数. (3)从函数返回时复制一个对象. (4)初始化顺序容器中的元素. (5)根据元素初始化式列表初始化数组元素. 下面分别对以上5点进行说明. 1.对象的定义式. C++支持两种初始化形式:直接初始化和复制初始化.复制初始

C++笔记(11):拷贝控制(拷贝移动,构造赋值,析构)

控制对象拷贝,赋值,析构 拷贝构造函数,移动构造函数 拷贝赋值运算符,移动赋值运算符 析构函数 ------------------------------------------------------------------------------------------------------------------------------------- 1. 拷贝构造函数:参数必须是引用类型&,一般是const的 拷贝构造函数的第1个参数指的是对于自身类类型的引用 2.拷贝赋值运算符:本

面向对象程序设计——抽象基类,访问控制与继承,继承中的类作用域,拷贝函数与拷贝控制

一.抽象基类 1)纯虚函数 和普通的虚函数不同,一个纯虚函数无须定义.我们通过在函数体的位置(即在声明语句的分号之前)书写=0就可以将一个虚函数说明为纯虚函数.其中,=0只能出现在类内部的虚函数声明语句处. 值得注意的是,我们也可以为纯虚函数提供定义,不过函数体必须定义在类的外部.也就是说,我们不能在类的内部为一个=0的函数提供函数体. 2)含有纯虚函数的类是抽象基类 含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类.抽象基类负责定义接口,而后续的其他类可以覆盖该接口.我们不能直接创建一个抽象

c++类的拷贝、赋值与销毁(拷贝构造函数、拷贝赋值运算符析构函数)

拷贝构造函数     如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数. 拷贝构造函数第一个参数必须是一个引用类型.此参数几乎总是一个const的引用.拷贝构造函数在几种情况下都会被隐式地使用.因此,拷贝构造函数通常不应该是explicit的. 合成拷贝构造函数 与合成默认构造函数不同,即使我们定义了其他构造函数,编译器也会为我们合成一个拷贝构造函数. 对某些类来说,合成拷贝构造函数用来阻止我们拷贝该类类型的对象.而一般情况,合成的拷贝构造函数

拷贝构造,深度拷贝,关于delete和default相关的操作,explicit,类赋初值,构造函数和析构函数,成员函数和内联函数,关于内存存储,默认参数,静态函数和普通函数,const函数,友元

 1.拷贝构造 //拷贝构造的规则,有两种方式实现初始化. //1.一个是通过在后面:a(x),b(y)的方式实现初始化. //2.第二种初始化的方式是直接在构造方法里面实现初始化. 案例如下: #include<iostream> //如果声明已经定义,边不会生成 class classA { private: int a; int b; public: //拷贝构造的规则,有两种方式实现初始化 //1.一个是通过在后面:a(x),b(y)的方式实现初始化 //2.第二种初始化的方式是直

C语言中的位拷贝与值拷贝浅谈(转载)

注:C语言实现的PHP变量的赋值过程中,就涉及到了 深拷贝和浅拷贝 位拷贝拷贝的是地址(也叫浅拷贝),而值拷贝则拷贝的是内容(深拷贝).深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝. 位拷贝,及"bitwise assignment"是指将一个对象的内存映像按位原封不动的复制给另一个对象,所谓值拷贝就是指,将原对象的值复制一份给新对象. 在用"bitwise assig

C++之拷贝构造与拷贝赋值

拷贝构造和拷贝赋值------一个有点难的问题 介绍之前,我们需要首先了解深拷贝与浅拷贝的差异: 何为深拷贝,深拷贝不会复制指针,而是令目标对象拥有独立的资源,该资源是从元对象中复制,即先找到对象的指针,在通过指针拷贝其内容: 何为浅拷贝,即之赋值指针的地址,不会赋值指针的目标,容易引发double free异常,即多个目标指向同一个内存: 缺省拷贝构造函数和缺省拷贝赋值函数 如果一个类没有显示的定义一个拷贝构造函数和拷贝赋值运算符,则编译器会为其默认提供一个,但是这个函数只能进行浅拷贝: 如果

【转载】C++中的位拷贝和值拷贝

---恢复内容开始--- 原文:C++中的位拷贝和值拷贝 原文:http://blog.csdn.net/liam1122/article/details/1966617 为了便于说明我们以String类为例: 首先定义String类,而并不实现其成员函数. Class String { public: String(const char *ch=NULL);//默认构造函数 String(const String &str);//拷贝构造函数 ~String(void); String &

控制生活就是学会控制自己的习惯

再多的化妆品也抵不上充足的睡眠和均衡的营养. 明知睡得晚肯定起不早,就要早一点睡.现在明白了习惯的作用是多么大,因此绝对不要打破平衡(比如某一天睡得晚),打破平衡之后重新找回平衡是很痛苦的. 好吧,早睡早起. 早饭:豆腐脑(最近早饭就啃馒头,胸都变小了.) 一个鸡蛋 一个肉包 一个馒头或菜包 明天又是新的一天,COME! 控制生活就是学会控制自己的习惯,布布扣,bubuko.com