• C++的句柄类


    上一篇文件介绍了关于C++代理类的使用场景和实现方法,但是代理类存在一定的缺陷,就是每个代理类会创建一个新的对象,无法避免一些不必要的内存拷贝,本篇文章引入句柄类,在保持代理类多态性的同时,还可以避免进行不不要的对象复制。
    我们先来看一个简易的字符串封装类:MyString,为了方便查看代码,将函数的声明和实现放到了一起。

    class MyString
    {
    public:
    	// 默认构造函数
    	MyString()
    	{
    		std::cout << "MyString()" << std::endl;
    
    		buf_ = new char[1];
    		buf_[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] = '';
    		}
    		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实现,这两种思想还是很经典的,垃圾回收、智能指针的实现都有借鉴这两种思想。水平有限,可能会有一些错误或者描述不明确,欢迎大家拍砖~~

  • 相关阅读:
    pcs7 opc 连接问题
    nuget 多个程序引用同一个库时,当个这个库更新时,引用的程序都要跟新,否则会在运行时出错
    nuget update FileConflictAction
    a
    C#版本
    .NET Framework版本与CLR版本之间的关系
    c# 将一种数组类型转成另一种数组类型
    tfs 清除缓存,在需要时
    java枚举和仿枚举
    java反射的用法
  • 原文地址:https://www.cnblogs.com/lzm-cn/p/9168439.html
Copyright © 2020-2023  润新知