上一篇文件介绍了关于 C++ 代理类的使用场景和实现方法, 但是代理类存在一定的缺陷, 就是每个代理类会创建一个新的对象, 无法避免一些不必要的内存拷贝, 本篇文章引入句柄类, 在保持代理类多态性的同时, 还可以避免进行不不要的对象复制.
我们先来看一个简易的字符串封装类: MyString, 为了方便查看代码, 将函数的声明和实现放到了一起.
- 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_;
- };
看一段测试程序
- #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;
- }
输出内容如下:
可以看到, 定义了三个 MyString 对象, str2 和 str3 都是由 str1 拷贝构造而来, 而且在程序的运行过程中, str2 和 str3 的内容并未被修改, 但是 str1 和 str2 已经复制了 str1 缓冲区的内容到自己的缓冲区中. 其实这里可以做一个优化, 就是让 str1 和 str2 在拷贝构造的时候, 直接指向 str1 的内存, 这样就避免了重复的内存拷贝. 但是这样又会引出一些新的问题:
1. 多个指针指向同一块动态内存, 内存改何时释放? 由谁释放?
2. 如果某个对象需要修改字符串中的内容, 该如和处理?
解决这些问题, 在 C++ 中有两个比较经典的方案, 那就是引用计数和 Copy On Write.
在引用计数中, 每一个对象负责维护对象所有引用的计数值. 当一个新的引用指向对象时, 引用计数器就递增, 当去掉一个引用时, 引用计数就递减. 当引用计数到零时, 该对象就将释放占有的资源.
下面给出引用计数的一个封装类:
- 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 对象的引用次数.
- 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_;
- };
看一段测试程序:
- 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++ 句柄类的设计思想与简单实现, 主要通过引用计数和 Copy On Write 实现, 这两种思想还是很经典的, 垃圾回收, 智能指针的实现都有借鉴这两种思想. 水平有限, 可能会有一些错误或者描述不明确, 欢迎大家拍砖~~
来源: https://www.cnblogs.com/lzm-cn/p/9168439.html