Back

/ 12 min read

C++ 智能指针之引用计数

实现一个固定类(String)的引用计数类StringRc(不允许使用泛型,所以只能支持特定类型)。

StringRc的功能是通过引用计数来管理String的资源,所以String本身存在需要在堆上(不理解后面解释吧)。StringRc可以进行clone签出新的不可变引用(此处不可变引用为设计层面,实际代码中我们无法做到不可变)。

实现

String

设计一个String类,其中需要实现String类的析构、拷贝构造(深拷贝)。

// String.h 文件
class String{
char* m_pBuffer = nullptr; // 字符串缓冲区
int m_nLength = 0; // 字符串长度 字符串以'\0'结尾 字符串长度不包含\0
int m_nBufferSize = 0; // 缓冲区大小
public:
String();
String(const char* str);
~String();
String(const String& str);
String(String&&); // 移动构造函数
}
// String.cpp 文件
String::String() {
m_pBuffer = new char[1];
m_pBuffer[0] = '\0';
m_nLength = 0;
m_nBufferSize = 1;
}
String::String(const char* str) {
m_nLength = strlen(str);
m_nBufferSize = m_nLength + 1;
m_pBuffer = new char[m_nBufferSize];
strcpy_s(m_pBuffer, m_nBufferSize, str);
}
String::~String() {
std::cout << "String的析构" << std::endl;
if (m_pBuffer != nullptr) {
delete[] m_pBuffer;
m_pBuffer = nullptr;
}
m_nBufferSize = 0;
m_nLength = 0;
}
String::String(const String& str) {
std::cout << "String 拷贝构造(深拷贝)!" << std::endl;
m_nLength = str.m_nLength;
m_nBufferSize = str.m_nBufferSize;
m_pBuffer = new char[m_nBufferSize];
strcpy_s(m_pBuffer, m_nBufferSize, str.m_pBuffer);
}

不要看代码,看我!

没时间解释了,我们只看最重要的。

  • String中定义一个字符串缓冲区m_pBuffer用来模拟管理一个堆内存。
  • String::String(const char* str)是为了我们能从一个字符串字面值便捷的创建一个String对象。
  • 析构和默认构造没什么好说的,创建和销毁我们没有做特别的事情。
  • String::String(const String& str)我们定义了一个拷贝构造函数,它的作用是以str为原型,创建一个新的String对象,两者资源不共享。(将str中的资源,深拷贝到新的String对象)。

StringRc

StringRc的功能是持有一个String类的资源,并且能通过clone方法进行签出新的StringRc对象。

通过StringRc A对象签出的StringRc B对象,两者共同持有一个String对象的资源。在设计层面我们不对外提供这个String对象的成员(不public这个String对象成员),来达到StringRc为一个资源的多个不可变引用。

StringRc.cpp
#include "StringRc.h"
StringRc::StringRc(String&& str) {
strRef = new String(str); // 智能指针来管理堆内存 深拷贝String 目的是为了将String本身移动到堆
strong_count = new uintmax_t(1); // 初始化引用计数
}
StringRc::~StringRc() {
*strong_count = *strong_count - 1;
if (*strong_count <= 0) {
SAFA_DELETE(strong_count);
SAFA_DELETE(strRef);
}
}
StringRc::StringRc(const StringRc& origin) {
// 浅拷贝 资源以及强引用计数器
strong_count = origin.strong_count;
*strong_count = *strong_count + 1;
strRef = origin.strRef;
}
StringRc StringRc::clone(StringRc& strRc) {
return StringRc(strRc); // 调用拷贝构造函数
}
uintmax_t StringRc::getStrongCountNum() {
return *strong_count;
}
StringRc.h
#pragma once
#include "String.h"
#include <cstdint>
#define SAFA_DELETE(x) if(x!=nullptr){delete x;}
/*
引用计数增加:
1. 新建Rc的时候(new的时候).
2. clone的时候. 实际上使用的是Rc的拷贝构造函数 在拷贝构造中增加引用计数
*/
class StringRc {
private:
uintmax_t* strong_count = nullptr; // 强引用计数
String* strRef = nullptr;
public:
StringRc(String&& str); // 移动构造一个引用计数器 强制资源丢到堆上
~StringRc();
StringRc(const StringRc& origin); // 拷贝构造
static StringRc clone(StringRc& strRc);
uintmax_t getStrongCountNum();
};

解析

  • 我们定义了一个公共构造方法StringRc::StringRc(String&& str)
    • 为什么我们要使用String的右值引用来构造StringRc?答:因为在设计上,我们希望StringRc获取这个String的所有权,也就是将原来的String变为只有StringRc对象持有,所以我们需要用显式的右值引用来告诉开发者,我们需要你资源的所有权。这样我们在各个StringRc的对象之间共享String资源的时候不会发生:
      • String资源的意外释放。(不转移所有权时,有没有可能有一个逻辑去主动释放String所持有的资源?有可能!)
      • String资源的意外修改。(理由同上,我们无法保证不存在外部的修改。我们不希望有外部的修改,因为这对于StringRc来说是一个黑盒操作,我们的所有引用计数指向的资源被意外的修改,这是反直觉的。)
    • 我们在构造中使用了String(str)来深拷贝这个资源,目的是为了将String本身移动到堆,因为我们并不清楚这个资源本身是存在堆还是栈,如果是栈,那么这个资源会被栈主动回收。当String本身被回收时,如果我们的StringRc没有被回收,就会出现悬垂引用。
      • 我们用生命周期来解释这个操作。因为StringRc来保存String的引用,所以String本身一定要比所有的StringRc活得更久!生命周期的重要规则:被引用者的生命周期一定要大于等于(或者不小于)引用者本身的生命周期。在实际应用中,我们不清楚StringRc本身的生命周期,它有可能要跨多个函数,所以一定要保证String不会被意外释放!
  • 接下来看我们的static StringRc clone(StringRc& strRc);方法。
    • clone是一个静态,设计目的是为了以StringRc& strRc这个智能指针为源,签出一个新的StringRc对象,让这两个StringRc同时指向一个String资源对象。
    • clone中,我们使用了拷贝构造方法StringRc(const StringRc& origin);,新签出的StringRc对象,浅拷贝strong_countstrRef,并使得strong_count强引用计数器进行增加1。
  • 最后我们看智能指针对象的析构方法~StringRc();
    • 析构方法很好理解,每当一个StringRc对象进行析构的时候,我们会通过减少它们公共的强引用计数器来进行无损耗析构。最终在所有引用都被销毁时(强引用计数器为0),手动执行资源String的析构,来释放堆内存。
image-20240523192541075

main函数开用!

main.cpp
#include <iostream>
#include "String.h"
#include "StringRc.h"
int main() {
using namespace std; // 养成良好习惯 不要扩大你所使用的命名空间范围
StringRc strRc(std::move(String("Hello World!"))); // 创建一个String资源,并使用此资源初始化引用计数智能指针
cout << "strRc.getStrongCountNum: " << strRc.getStrongCountNum() << endl; // 看一眼计数
StringRc strRc2 = StringRc::clone(strRc); // 签出一个
// 看一下他们两个的强引用计数器
cout << "strRc.getStrongCountNum: " << strRc.getStrongCountNum() << endl;
cout << "strRc2.getStrongCountNum: " << strRc2.getStrongCountNum() << endl;
return 0;
}
image-20240523193918978

内存就不看了吧,相信你已经人机合一,代码眼上走,内存心中留。


冒充写时拷贝

这章已经难以实现了,为什么?

理想情况下我们应该这么使用LazyCopy对象:

int main(){
StringRc strRc1 = StringRc(LazyCopy(String("Hello World!")));
// LazyCopy 需要智能解引用
strRc1.get_mut().concat('A');
// strRc1.get_mut() 得到的是LazyCopy对象
// LazyCopy需要实现智能解引用 重载*运算符,所以对LazyCopy对象 .运算 得到的应该是String对象
return 0;
}
  • 如果我们托管一个LazyCopy类用来包裹String,那么我们的StringRc必须接受一个泛型T where LatyCopy||String,泛型T必须接收LazyCopy或者String两个不同对象,不使用泛型语法的情况下,我们很难实现。使用c语法强转?算了吧我不想写屎山。
  • 不允许使用重载,不允许使用泛型。man what can i say!

综上所述,我们如果想将写时拷贝与String解耦实现。那么我们必须在可以重载、泛型的情况下。所以,我们现在只能冒充一下写时拷贝。

StringRc.cpp
// 为我们的StringRc类新增一个get_mut_rc方法
StringRc StringRc::get_mut_rc() {
*strong_count = *strong_count - 1; // 原来的引用计数减一
strRef = new String(*strRef); // 深拷贝出来
strong_count = new uintmax_t(1); // 创建新的引用计数
return *this;
}
  • get_mut_rc方法将此StringRc对象从此计数中移除,并创建新的计数。同时深拷贝String资源对象,实现资源完全分离。
main.cpp
#include <iostream>
#include "String.h"
#include "StringRc.h"
int main() {
using namespace std;
StringRc strRc(std::move(String("Hello World!")));
cout << "strRc.getStrongCountNum: " << strRc.getStrongCountNum() << endl;
cout << endl;
StringRc strRc2 = StringRc::clone(strRc);
cout << "strRc.getStrongCountNum: " << strRc.getStrongCountNum() << endl;
cout << "strRc2.getStrongCountNum: " << strRc2.getStrongCountNum() << endl;
cout << endl;
strRc2 = strRc2.get_mut_rc(); // 深拷贝出来一个引用计数器 变相的实现了lazyCopy吧 手动的
cout << "strRc.getStrongCountNum: " << strRc.getStrongCountNum() << endl;
cout << "strRc2.getStrongCountNum: " << strRc2.getStrongCountNum() << endl;
cout << endl;
StringRc strRc3 = StringRc::clone(strRc2);
cout << "strRc.getStrongCountNum: " << strRc.getStrongCountNum() << endl;
cout << "strRc2.getStrongCountNum: " << strRc2.getStrongCountNum() << endl;
cout << "strRc3.getStrongCountNum: " << strRc3.getStrongCountNum() << endl;
cout << endl;
return 0;
}
image-20240523201454830

上面的代码也好理解。

  • 我们先通过strRc创建了一个引用计数。并clone出来一个strRc2此时,两者的引用计数都为2,且指向同一资源。
  • 执行strRc2.get_mut_rc()方法,将strRc2从原有的引用计数中移除,并创建新的引用计数和资源。此时strRcstrRc2的引用计数都为1(但不是同一个引用计数器)。
  • 通过strRc2签出strRc3,此时strRc2strRc3引用计数都为2,并且指向同一个String资源。而strRc引用计数不变,仍为1
  • 最后,所有的引用计数都被栈回收,strRcString资源和strRc2、strRc3String资源都被回收,执行两次String的析构方法。

写前豪言壮语,写的时候才意识到没法使用泛型和运算符重载,只能悻悻而归。