详解C++句柄类

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

(0)

相关推荐

  • 关于C++友元类的实现讲解

    C++中的友元既可以实现友元函数,也可以实现友元类,也就是说一个类也可以作为另外一个类的友元.当作为一个类的友元时,它的所有成员函数都是另一个类的友元函数,都可以访问另一个类的私有或者公有成员. 请看实例: #include <iostream> #include <cstring> using namespace std ; //声明教师类 class Techer ; //学生类 class Student { private: string name ; int age ;

  • C++实现日期类(Date)

    本文实例为大家分享了C++实现日期类的具体代码,供大家参考,具体内容如下 #include<iostream> #include<stdlib.h> using namespace std; class Date { public: //构造函数 Date(int year = 1900, int month = 1, int day = 1) :_year(year) , _month(month) , _day(day) { if (!IsInvalidDate(_year,

  • C/C++中的sizeof运算符和size_t类型的详解

    sizeof的作用 sizeof是c的运算符之一,用于获取操作数被分配的内存空间,以字节单位表示. 这里指的操作数,可以是变量,也可以是数据类型,如int,float等.所以就可以通过它来获取本地c库定义的基本类型的范围. sizeof的使用 1.对于一般变量,形式2种:sizeof a 或 sizeof(a); 2.对于数据类型,必须使用带括号的方式,如sizeof(int). size_t的说明 size_t是标准C库中定义的,应为unsigned int,在64位系统中为 long uns

  • 解决易语言转换到C++ 自定义数据类型

    自定义数据类型如下 .版本 2 .数据类型 数据 .成员 坐标, 坐标_数据类型 .数据类型 坐标_数据类型 .成员 x, 小数型 .成员 z, 小数型 .成员 y, 小数型 这里的自定义数据类型下的"数据"类型下的"坐标"成员引用自定义数据类型"坐标_数据类型" 子程序如下 .版本 2 .子程序 自己数据 .参数 返回数据, 数据, 参考 返回数据.坐标.x = 1 返回数据.坐标.z = 2 返回数据.坐标.y = 3 这里的子程序内的参数&

  • 关于C++内部类的介绍与使用示例

    介绍 1.把一个类定义在另一个类的内部,称里面的类为内部类. 例如: class A { public: class B { public: int x; int y; }; }; 类B即为内部类. 2.内部类和外部类相互没有特权,即外部类无法自由访问内部类,内部类也无法自由访问外部类. a.他们不是朋友关系 b.他们不是父子关系 内部类的使用 #include <stdio.h> class A { public: class B { public: void test() { printf

  • C++标准模板库string类的介绍与使用讲解

    介绍 c++中字符串string对象属于一个类,内置了很多实用的成员函数,操作简单,方便更直观. 命名空间为std,所属头文件<string> 注意:不是<string.h>. 跟进代码会发现string其实只是basic_string模板类的一个typedef. 赋值 //方法1 string str1 = "woniu201"; //方法2 char* p = "woniu201"; string str2 = p; 遍历 //方法1 使

  • C++关于构造函数可向父类或者本类传参的讲解

    前面我们学习了C++使用初始化列表的方式来初始化字段的方法: https://www.jb51.net/article/153032.htm 这一节的原理和前面的差不多. 在C++的构造函数中,子类继承父类,那么,在创建一个子类成员时,可以同时向父类或者子类的构造函数进行传参,实现方法如下: 写一个例子:mul_argc.c #include <iostream> #include <cstring> using namespace std ; //英雄联盟类 class Hero

  • C++类的分离式写法介绍示例

    介绍 类的分离式写法,使得代码更加规范,增强了阅读性. 分离式写法的规则: 1.类的变量:写在类的里面 2.成员函数:类中写函数的声明,函数的定义写在类体外. 3.写在类外函数定义时,类名前加限定(Object: :),其中,::理解为表示范围的符号. 代码演示 头文件:Object.h #ifndef _OBJECT_H_ #define _OBJECT_H_ class Student { private: char name[32]; int age; public: void SetNa

  • C++ 读取文件内容到指定类型的变量方法

    如下所示: #include <iostream> #include <fstream> #include <sstream> #include <string> using namespace std; int main(){ cout << "input the file name: "; string file_name; cin >> file_name; cout << endl; // if

  • C++11并发编程关于原子操作atomic的代码示例

    一:概述 项目中经常用遇到多线程操作共享数据问题,常用的处理方式是对共享数据进行加锁,如果多线程操作共享变量也同样采用这种方式. 为什么要对共享变量加锁或使用原子操作?如两个线程操作同一变量过程中,一个线程执行过程中可能被内核临时挂起,这就是线程切换,当内核再次切换到该线程时,之前的数据可能已被修改,不能保证原子操作. C++11提供了个原子的类和方法atomic,保证了多线程对变量原子性操作,相比加锁机制mutex.lock(),mutex.unlock(),性能有几倍的提升. 所需头文件<a

随机推荐