详解C++句柄类

时间:2022-09-30 10:07:17

上一篇文件介绍了关于C++代理类的使用场景和实现方法,但是代理类存在一定的缺陷,就是每个代理类会创建一个新的对象,无法避免一些不必要的内存拷贝,本篇文章引入句柄类,在保持代理类多态性的同时,还可以避免进行不不要的对象复制。

我们先来看一个简易的字符串封装类:MyString,为了方便查看代码,将函数的声明和实现放到了一起。

?
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
class MyString
{
public:
 // 默认构造函数
 MyString()
 {
  std::cout << "MyString()" << std::endl;
 
  buf_ = new char[1];
  buf_[0] = '\0';
  len_ = 0;
 }
 
 // const char*参数的构造函数
 MyString(const char* str)
 {
  std::cout << "MyString(const char* str)" << std::endl;
 
  if (str == nullptr)
  {
   len_ = 0;
   buf_ = new char[1];
   buf_[0] = '\0';
  }
  else
  {
   len_ = strlen(str);
   buf_ = new char[len_ + 1];
   strcpy_s(buf_, len_ + 1, str);
  }
 }
 
 // 拷贝构造函数
 MyString(const MyString& other)
 {
  std::cout << "MyString(const MyString& other)" << std::endl;
 
  len_ = strlen(other.buf_);
  buf_ = new char[len_ + 1];
  strcpy_s(buf_, len_ + 1, other.buf_);
 }
 
 // str1 = str2;
 const MyString& operator=(const MyString& other)
 {
  std::cout << "MyString::operator=(const MyString& other)" << std::endl;
 
  // 判断是否为自我赋值
  if (this != &other)
  {
   if (other.len_ > this->len_)
   {
    delete[]buf_;
    buf_ = new char[other.len_ + 1];
   }
 
   len_ = other.len_;
   strcpy_s(buf_, len_ + 1, other.buf_);
  }
 
  return *this;
 }
 
 // str = "hello!";
 const MyString& operator=(const char* str)
 {
  assert(str != nullptr);
 
  std::cout << "operator=(const char* str)" << std::endl;
 
  size_t strLen = strlen(str);
  if (strLen > len_)
  {
   delete[]buf_;
   buf_ = new char[strLen + 1];
  }
 
  len_ = strLen;
  strcpy_s(buf_, len_ + 1, str);
  
  return *this;
 }
 
 // str += "hello"
 void operator+=(const char* str)
 {
  assert(str != nullptr);
 
  std::cout << "operator+=(const char* str)" << std::endl;
 
  if (strlen(str) == 0)
  {
   return;
  }
 
  size_t newBufLen = strlen(str) + len_ + 1;
  char* newBuf = new char[newBufLen];
  strcpy_s(newBuf, newBufLen, buf_);
  strcat_s(newBuf, newBufLen, str);
 
  delete[]buf_;
  buf_ = newBuf;
 
  len_ = strlen(buf_);
 }
 
 // 重载 ostream的 <<操作符 ,支持 std::cout << MyString 的输出
 friend std::ostream& operator<<(std::ostream &out, MyString& obj)
 {
  out << obj.c_str();
  return out;
 }
 
 // 返回 C 风格字符串
 const char* c_str()
 {
  return buf_;
 }
 
 // 返回字符串长度
 size_t length()
 {
  return len_;
 }
 
 ~MyString()
 {
  delete[]buf_;
  buf_ = nullptr;
 }
 
private:
 char* buf_;
 size_t len_;
};

看一段测试程序

?
1
2
3
4
5
6
7
8
9
10
11
12
#include "MyString.h"
 
int _tmain(int argc, _TCHAR* argv[])
{
 MyString str1("hello~~");
 MyString str2 = str1;
 MyString str3 = str1;
 
 std::cout << "str1=" << str1 << ", str2=" << str2 << ", str3=" << str3;
 
 return 0;
}

输出内容如下:

详解C++句柄类

可以看到,定义了三个MyString对象,str2和str3都是由str1拷贝构造而来,而且在程序的运行过程中,str2和str3的内容并未被修改,但是str1和str2已经复制了str1缓冲区的内容到自己的缓冲区中。其实这里可以做一个优化,就是让str1和str2在拷贝构造的时候,直接指向str1的内存,这样就避免了重复的内存拷贝。但是这样又会引出一些新的问题:

1. 多个指针指向同一块动态内存,内存改何时释放?由谁释放?

2. 如果某个对象需要修改字符串中的内容,该如和处理?

解决这些问题,在C++中有两个比较经典的方案,那就是引用计数和Copy On Write。

在引用计数中,每一个对象负责维护对象所有引用的计数值。当一个新的引用指向对象时,引用计数器就递增,当去掉一个引用时,引用计数就递减。当引用计数到零时,该对象就将释放占有的资源。

下面给出引用计数的一个封装类:

?
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
class RefCount
{
public:
 
 RefCount() : count_(new int(1)){};
 
 RefCount(const RefCount& other) : count_(other.count_)
 {
  ++*count_;
 }
 
 ~RefCount()
 {
  if (--*count_ == 0)
  {
   delete count_;
   count_ = nullptr;
  }
 }
 
 bool Only()
 {
  return *count_ == 1;
 }
 
 void ReAttach(const RefCount& other)
 {
  // 更新原引用计数的信息
  if (Only())
  {
   delete count_;
  }
  else
  {
   --*count_;
  }
 
  // 更新新的引用计数的信息
  ++*other.count_;
  
  // 绑定到新的引用计数
  count_ = other.count_;
 }
 
 void MakeNewRef()
 {
  if (*count_ > 1)
  {
   --*count_;
   count_ = new int(1);
  }
 }
 
private:
 int* count_;
};

Copy On Write:就是写时复制,通过拷贝构造初始化对象时,并不直接将参数的资源往新的对象中复制一份,而是在需要修改这些资源时,将原有资源拷贝过来,再进行修改,就避免了不必要的内存拷贝。

下面的代码是完整的句柄类MyStringHandle。每一个句柄类,都包含一个引用计数的类,用来管理和记录对MyString对象的引用次数。

?
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
class MyStringHandle
{
public:
 MyStringHandle() : pstr_(new MyString){}
 
 // 这两种参数的构造函数必须构造一个新的MyString对象出来
 MyStringHandle(const char* str) : pstr_(new MyString(str)) {}
 MyStringHandle(const MyString& other) : pstr_(new MyString(other)) {}
 
 // 拷贝构造函数,将指针绑定到参数绑定的对象上,引用计数直接拷贝构造,在拷贝构造函数内更新引用计数的相关信息
 MyStringHandle(const MyStringHandle& ohter) : ref_count_(ohter.ref_count_), pstr_(ohter.pstr_) {}
 
 ~MyStringHandle()
 {
  if (ref_count_.Only())
  {
   delete pstr_;
   pstr_ = nullptr;
  }
 }
 
 MyStringHandle& operator=(const MyStringHandle& other)
 {
  // 绑定在同一个对象上的句柄相互赋值,不作处理
  if (other.pstr_ == pstr_)
  {
   return *this;
  }
 
  // 若当前引用唯一,则销毁当前引用的MyString
  if (ref_count_.Only())
  {
   delete pstr_;
  }
 
  // 分别将引用计数和对象指针重定向
  ref_count_.ReAttach(other.ref_count_);
  pstr_ = other.pstr_;
 
  return *this;
 }
 
 // str = "abc" 这里涉及到对字符串内容的修改,
 MyStringHandle& operator=(const char* str)
 {
  if (ref_count_.Only())
  {
   // 如果当前句柄对MyString对象为唯一的引用,则直接操作改对象进行赋值操作
   *pstr_ = str;
  }
  else
  {
   // 如果不是唯一引用,则将原引用数量-1,创建一个新的引用,并且构造一个新的MyString对象
   ref_count_.MakeNewRef();
   pstr_ = new MyString(str);
  }
 
  return *this;
 }
 
private:
 MyString* pstr_;
 RefCount ref_count_;
};

看一段测试程序:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int _tmain(int argc, _TCHAR* argv[])
{
 // 构造MyString
 MyStringHandle str1("hello~~");
 
 // 不会构造新的MyString
 MyStringHandle str2 = str1;
 MyStringHandle str3 = str1;
 MyStringHandle str4 = str1;
 
 // 构造一个空的MyString
 MyStringHandle str5;
 
 // 将str1赋值到str5,不会有内存拷贝
 str5 = str1;
 
 // 修改str5的值
 str5 = "123";
 str5 = "456";
 
 return 0;
}

详解C++句柄类

总结

本篇文章介绍了C++句柄类的设计思想与简单实现,主要通过引用计数和Copy On Write实现,这两种思想还是很经典的,垃圾回收、智能指针的实现都有借鉴这两种思想。水平有限,可能会有一些错误或者描述不明确,欢迎大家拍砖~~

原文链接:https://www.cnblogs.com/lzm-cn/p/9168439.html