10. 引用计数

1. 引用计数

1.1 作用

  • 在实际编程中,堆对象可能被传递给另一个对象,一旦这个过程复杂了,我们最后很难确定谁拥有了这个对象。使用引用计数就可以抛开这个问题,不需要再去关心谁拥有了这个对象,我们把管理权交给了对象自己,当对象不再被引用时,它自己负责释放自己。
  • 解决了同一个对象存在多份相同内存的问题。引用计数可以让等值对象共享一份数据实体,可以节省内存,程序速度加快,因为不再需要构造和析构同值对象的多余副本。

1.2 等值对象具有多份复制的情况

cpp 复制代码
#include <iostream>
#include <string.h>

using namespace std;

class String {
public:
	String(const char* value = "") {
		data = new char[strlen(value) + 1];
		strcpy(data, value);
	}

	String& operator=(const String& rhs) {
		if (this == & rhs) {
			return *this;
		}

		delete[] data;

		data = new char[strlen(rhs.data) + 1];
		strcpy(data, rhs.data);
		
		return *this;
	}

	char* data;
};

int main() {
	String a, b, c, d, e;

	a = b = c = d = e = "Hello";

	cout << "a.data=" << (void*)a.data << endl;
	cout << "b.data=" << (void*)b.data << endl;
	cout << "c.data=" << (void*)c.data << endl;
	cout << "d.data=" << (void*)d.data << endl;
	cout << "e.data=" << (void*)e.data << endl;

	return 0;
}
bash 复制代码
$ ./a.out
a.data=0x634ec5a77eb0
b.data=0x634ec5a77ed0
c.data=0x634ec5a77ef0
d.data=0x634ec5a77f10
e.data=0x634ec5a77f30
$

下一个代码:

cpp 复制代码
#include <iostream>
#include <string.h>

using namespace std;

class String {
private:
    struct StringValue {
        int refCount;     // 引用计数
        char* data;       // 实际字符串存储空间

        StringValue(const char* initValue);
        ~StringValue();
    };

    StringValue* value;   // 指向共享数据的指针

public:
    String(const char* initValue = "");
    String(const String& rhs);
    String& operator=(const String& rhs);
    ~String();

    const char* c_str() const { return value->data; }
    int use_count() const { return value->refCount; }
    const void* data_addr() const { return static_cast<const void*>(value->data); }
};

// 初始化StringValue
String::StringValue::StringValue(const char* initValue):refCount(1) {
    data = new char[strlen(initValue) + 1];
    strcpy(data, initValue);
    cout << "[构造 StringValue] data地址=" << (void*)data 
    << " 内容=\"" << data << "\" refCount=" << refCount << endl;
}

// 析构StringValue
String::StringValue::~StringValue() {
    cout << "[释放 StringValue] data地址=" << (void*)data 
        << " 内容=\"" << data << "\"" << endl;
    delete[] data;
}

// 构造函数
String::String(const char* initValue):value(new StringValue(initValue)) {
    cout << "String(const char*) -> 新建对象 value=" << value << endl;
}

// 拷贝构造
String::String(const String& rhs):value(rhs.value) {
    value->refCount++;
    cout << "String(const String&) -> 拷贝构造,共享 data地址=" << (void*)value->data 
        << " refCount=" << value->refCount << endl;
}

// 赋值运算符
String& String::operator=(const String& rhs) {
    if (this->value == rhs.value) {
        return *this;
    }
    if (--value->refCount == 0) {
        delete value;
    }
    value = rhs.value;
    ++value->refCount;

    cout << "operator= -> 赋值,共享 data地址=" << (void*)value->data 
        << " refCount=" << value->refCount << endl;
    return *this;
}

// 析构函数
String::~String() {
    if (--value->refCount == 0) {
        delete value;
    } else {
        cout << "析构但共享存在 -> data地址=" << (void*)value->data 
            << " 当前refCount=" << value->refCount << endl;
    }
}

// main 测试
int main() {
    cout << "=== 创建 s1 ===" << endl;
    String s1("Hello");

    cout << "\n=== 复制构造 s2(s1) ===" << endl;
    String s2(s1);

    cout << "\n=== 赋值操作 s3 = s1 ===" << endl;
    String s3;
    s3 = s1;

    cout << "\n=== 打印地址对比 ===" << endl;
    cout << "s1.data=" << s1.data_addr() << " refCount=" << s1.use_count() << endl;
    cout << "s2.data=" << s2.data_addr() << " refCount=" << s2.use_count() << endl;
    cout << "s3.data=" << s3.data_addr() << " refCount=" << s3.use_count() << endl;

    cout << "\n=== 结束 main,开始析构 ===" << endl;
    return 0;
}
bash 复制代码
$ ./a.out
=== 创建 s1 ===
[构造 StringValue] data地址=0x61d315d1a2e0 内容="Hello" refCount=1
String(const char*) -> 新建对象 value=0x61d315d1a2c0

=== 复制构造 s2(s1) ===
String(const String&) -> 拷贝构造,共享 data地址=0x61d315d1a2e0 refCount=2

=== 赋值操作 s3 = s1 ===
[构造 StringValue] data地址=0x61d315d1a320 内容="" refCount=1
String(const char*) -> 新建对象 value=0x61d315d1a300
[释放 StringValue] data地址=0x61d315d1a320 内容=""
operator= -> 赋值,共享 data地址=0x61d315d1a2e0 refCount=3

=== 打印地址对比 ===
s1.data=0x61d315d1a2e0 refCount=3
s2.data=0x61d315d1a2e0 refCount=3
s3.data=0x61d315d1a2e0 refCount=3

=== 结束 main,开始析构 ===
析构但共享存在 -> data地址=0x61d315d1a2e0 当前refCount=2
析构但共享存在 -> data地址=0x61d315d1a2e0 当前refCount=1
[释放 StringValue] data地址=0x61d315d1a2e0 内容="Hello"
$

1.3 存在的问题

之前引用计数有个问题:多个 String 对象共享一个底层缓冲区,任何一个对象修改字符串时都会影响到所有对象。

  • 写时复制机制 Copy-on-Write
    只有在"写操作"发生时,才真正复制数据,从而避免无谓的深拷贝。

关键思想:

  • 多个对象共享同一份字符串(引用计数大于 1 时)。
  • 当某个对象想要修改内容时,
    • 如果发现 refCount > 1:
      • 先把当前数据拷贝一份
      • 把旧的引用计数减去 1
      • 自己独享新的副本
    • 然后再进行修改

代码改进:

cpp 复制代码
#include <iostream>
#include <string.h>

using namespace std;

class String {
private:
    struct StringValue {
        int refCount;     // 引用计数
        char* data;       // 实际字符串存储空间

        StringValue(const char* initValue);
        ~StringValue();
    };

    StringValue* value;   // 指向共享数据的指针

public:
    String(const char* initValue = "");
    String(const String& rhs);
    String& operator=(const String& rhs);
    ~String();

    const char* c_str() const { return value->data; }
    int use_count() const { return value->refCount; }
    const void* data_addr() const { return static_cast<const void*>(value->data); }

    // 重载下标操作符
    const char& operator[](size_t index) const; // 只读版本
    char& operator[](size_t index); // 可写版本
};

// 初始化StringValue
String::StringValue::StringValue(const char* initValue):refCount(1) {
    data = new char[strlen(initValue) + 1];
    strcpy(data, initValue);
}

// 析构StringValue
String::StringValue::~StringValue() {
    delete[] data;
}

// 构造函数
String::String(const char* initValue):value(new StringValue(initValue)) {
}

// 拷贝构造
String::String(const String& rhs):value(rhs.value) {
    value->refCount++;
}

// 赋值运算符
String& String::operator=(const String& rhs) {
    if (this->value == rhs.value) {
        return *this;
    }
    if (--value->refCount == 0) {
        delete value;
    }
    value = rhs.value;
    ++value->refCount;

    return *this;
}

// 析构函数
String::~String() {
    if (--value->refCount == 0) {
        delete value;
    }
}

const char& String::operator[](size_t index) const {
    return value->data[index];
}

char& String::operator[](size_t index) {
    if (value->refCount > 1) {
        value->refCount--;
        value = new StringValue(value->data);
    }

    return value->data[index];
}

int main() {
    String a("Hello");
    String b(a);
    String c = b;

    cout << "=== 初始状态 ===" << endl;
    cout << "a: " << a.c_str() << " addr=" << a.data_addr() << " ref=" << a.use_count() << endl;
    cout << "b: " << b.c_str() << " addr=" << b.data_addr() << " ref=" << b.use_count() << endl;
    cout << "c: " << c.c_str() << " addr=" << c.data_addr() << " ref=" << c.use_count() << endl;

    cout << "\n=== 修改 b[0] = 'h' ===" << endl;
    b[0] = 'h';  // 触发写时复制

    cout << "\n=== 修改后状态 ===" << endl;
    cout << "a: " << a.c_str() << " addr=" << a.data_addr() << " ref=" << a.use_count() << endl;
    cout << "b: " << b.c_str() << " addr=" << b.data_addr() << " ref=" << b.use_count() << endl;
    cout << "c: " << c.c_str() << " addr=" << c.data_addr() << " ref=" << c.use_count() << endl;

    return 0;
}
bash 复制代码
$ ./a.out
=== 初始状态 ===
a: Hello addr=0x60f6a530fed0 ref=3
b: Hello addr=0x60f6a530fed0 ref=3
c: Hello addr=0x60f6a530fed0 ref=3

=== 修改 b[0] = 'h' ===

=== 修改后状态 ===
a: Hello addr=0x60f6a530fed0 ref=2
b: hello addr=0x60f6a5310320 ref=1
c: Hello addr=0x60f6a530fed0 ref=2
$
相关推荐
帅中的小灰灰2 分钟前
C++编程观察者设计模式
数据库·c++·设计模式
MSTcheng.1 小时前
【C++STL】priority_queue 模拟实现与仿函数实战
开发语言·c++
还有几根头发呀1 小时前
从 C++ 的角度,系统地解释 进程(Process)、线程(Thread)、协程(Coroutine) 的概念、原理、优缺点,以及常见应用场景。
c++
oioihoii1 小时前
Python与C++:从哲学到细节的全面对比
c++
小年糕是糕手1 小时前
【C++】C++入门 -- inline、nullptr
linux·开发语言·jvm·数据结构·c++·算法·排序算法
kk哥88991 小时前
Keil MDK 5.39 编程 + 调试 ,ARM 嵌入式开发!如何安装
c++·arm
重启的码农2 小时前
enet源码解析 (2) 对等节点 (ENetPeer)
c++·网络协议
塞北山巅2 小时前
camera hal层(AF)
c++·camera
qq_479875432 小时前
X-Macros(2)
c++
列逍2 小时前
深入理解 C++ 异常:从概念到实战的全面解析
开发语言·c++