10. 引用计数
1. 引用计数
1.1 作用
- 在实际编程中,堆对象可能被传递给另一个对象,一旦这个过程复杂了,我们最后很难确定谁拥有了这个对象。使用引用计数就可以抛开这个问题,不需要再去关心谁拥有了这个对象,我们把管理权交给了对象自己,当对象不再被引用时,它自己负责释放自己。
- 解决了同一个对象存在多份相同内存的问题。引用计数可以让等值对象共享一份数据实体,可以节省内存,程序速度加快,因为不再需要构造和析构同值对象的多余副本。
1.2 等值对象具有多份复制的情况
#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;
}
$ ./a.out
a.data=0x634ec5a77eb0
b.data=0x634ec5a77ed0
c.data=0x634ec5a77ef0
d.data=0x634ec5a77f10
e.data=0x634ec5a77f30
$
下一个代码:
#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;
}
$ ./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
- 自己独享新的副本
- 然后再进行修改
- 如果发现 refCount > 1:
代码改进:
#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;
}
$ ./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
$
