上一篇文件介绍了关于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实现,这两种思想还是很经典的,垃圾回收、智能指针的实现都有借鉴这两种思想。水平有限,可能会有一些错误或者描述不明确,欢迎大家拍砖~~

C++的句柄类的更多相关文章

  1. code of C/C++(3) - 从 《Accelerated C++》源码学习句柄类

    0  C++中多态的概念 多态是指通过基类的指针或者引用,利用虚函数机制,在运行时确定对象的类型,并且确定程序的编程策略,这是OOP思想的核心之一.多态使得一个对象具有多个对象的属性.class Co ...

  2. C++中的句柄类

    初次在<C++ Primer>看到句柄,不是特别理解.在搜索相关资料后,终于有了点头绪. 首先明白句柄要解决什么问题.参考文章<C++ 沉思录>阅读笔记——代理类 场景: 我们 ...

  3. c++ 容器、继承层次、句柄类

    一.容器与继承 在容器中保存有继承关系的对象,如果定义成保存基类对象,则派生类将被切割,如果定义成保存派生类对象,则保存基类对象又成问题(基类对象将被强制转换成派生类对象,而派生类中定义的成员未被初始 ...

  4. C++ 句柄类

    一.容器与继承 在容器中保存有继承关系的对象时,如果定义成保存基类对象,则派生类将被切割,如果定义成保存派生类对象,则保存基类对象又成问题(基类对象将被强制转换成派生类对象,而派生类中定义的成员未被初 ...

  5. C++ Primer 学习笔记_72_面向对象编程 --句柄类与继承[续]

    面向对象编程 --句柄类与继承[续] 三.句柄的使用 使用Sales_item对象能够更easy地编写书店应用程序.代码将不必管理Item_base对象的指针,但仍然能够获得通过Sales_item对 ...

  6. C++ 句柄类的原理以及设计

    句柄类存在的意义是为了弥补将派生类对象赋给基类对象时发生的切片效应.比如以下的程序: multimap<Base> basket; Base base; Derived derive; b ...

  7. C++中代理类和句柄类

    指针是 C 与其他语言区别的重要特征之一,在 C++ 中,指针也被广泛运用,我们通过指针实现多态.然而,众所周知,指针的使用必须小心,否则很容易造成内存泄漏 Memory Leak.当我们有几个指针指 ...

  8. c++句柄设计

    句柄,也称为智能指针. 我计算了一下我的时间,以后每14天得读完一本书,才不愧对我买的这么多书.然而我还要抽出时间来谢谢博文.最近读的是c++沉思录,开篇就用了3章来讲述句柄.好了,废话少说,接下来谈 ...

  9. sp<> 强指针类的用法

    在android 中可以广泛看到的template<typename T>,  class Sp 句柄类实际上是android 为实现垃圾回收机制的智能指针.智能指针是c++ 中的一个概念 ...

随机推荐

  1. [LeetCode]25. Reverse Nodes in k-Group k个一组翻转链表

    Given a linked list, reverse the nodes of a linked list k at a time and return its modified list. k ...

  2. python之高阶函数map/reduce

    L = [] for n in [1, 2, 3, 4, 5, 6, 7, 8, 9]: L.append(f(n)) print(L) Python内建了map()和reduce()函数. 我们先看 ...

  3. Django——stark组件

    stark组件是仿照django的admin模块开发的一套组件,它的作用是在网页上对注册的数据表进行增删改查操作. 一.配置 1.创建stark应用,在settings.py中注册stark应用 st ...

  4. TopcoderSRM679 Div1 250 FiringEmployees(树形dp)

    题意 [题目链接]这怎么发链接啊..... 有一个 \(n\) 个点的树,每个点有点权(点权可能为负) ,求包含点\(1\)的最 大权连通子图(的权值和) . \(n \leqslant 2500\) ...

  5. html相对定位绝对定位

    孔子说:“温故而知新,可以为师矣.”这几天参加了一个免费的前端课,每天晚上都有直播,讲解一个独立的案例.在听前端基础的时候,发现自己有不少东西没学会,平时在学校虽说html也写了不少,但有好大一部分都 ...

  6. html5格式样式

    <b>        加粗 <b style="font-size: 100px;">大字体</b>

  7. Java 重写hashCode() 时为什么要用 31 来计算

    在OSChina 中看到了一篇文章<Java 中正确使用 hashCode 和 equals 方法>,看到 hashCode 的方法体内的31比较有意思. 在Stackoverflow上找 ...

  8. 任务六:通过HTML及CSS模拟报纸排版

    任务目的 深入掌握CSS中的字体.背景.颜色等属性的设置 进一步练习CSS布局 任务描述 参考 PDS设计稿(点击下载),实现页面开发,要求实现效果与 样例(点击查看) 基本一致 页面中的各字体大小, ...

  9. SQL Server ->> 生成时间类型的Partition Function和Partition Scheme代码

    有时工作中要建个分区函数,可是像日期这种分区函数要是搞个几百个的值那不是要搞死我.于是写了点代码自动生成一个从1990年开始的按月的分区函数和对应的分区主题 USE [TestDB] GO DECLA ...

  10. HCNA-RIP定时器

    1.拓扑图 2. 1.RIP有哪些定时器?三种:更新定时器.老化定时器.垃圾回收定时器 2.RIP的定时器有哪些作用?更新定时器(30s):运行RIP的路由器会以30s为周期,向邻居发送RIP路由.老 ...