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
$
相关推荐
yolo_guo4 小时前
opencv 学习: 04 通过ROI处理图片局部数据,以添加水印为例
linux·c++·opencv
顺顺 尼5 小时前
模板进阶和array
c++
一匹电信狗5 小时前
【牛客CM11】链表分割
c语言·开发语言·数据结构·c++·算法·leetcode·stl
困鲲鲲5 小时前
ROS2系列 (10) : C++话题通信节点——发布者示例
c++·ros2
..过云雨5 小时前
11.【Linux系统编程】文件系统详解——从磁盘硬件到文件系统
linux·c++·后端·缓存
码住懒羊羊5 小时前
【C++】模板进阶 | 继承
android·java·c++
yong99905 小时前
C++语法—类的声明和定义
开发语言·c++·算法
狂奔的sherry5 小时前
构造/析构/赋值运算理解
开发语言·c++
大佬,救命!!!5 小时前
C++多线程运行整理
开发语言·c++·算法·学习笔记·多线程·新手练习