使用过智能指针的朋友都知道,有些智能指针是通过引用计数来实现的,那引用计数到底是什么呢,他是如何实现?下面我们就来详细讲解
引用计数是一种内存管理技术。我们以下面自定义的String类为例:
class String {
public:
~String() { if (val_) { delete[] val_; }}
String(const char* val) : String(val, val ? strlen(val) : 0) {}
String(const String& str) {
/*引用计数*/
}
String& operator=(const String& str) {
/*引用计数*/
return *this;
}
String(const char* val, int len) {
if (val && len > 0) {
val_ = new char[len + 1]();
std::copy_n(val, len, val_);
}
}
private:
char* val_ = nullptr;
};
我们思考一下,如何才能在拷贝构造和赋值运算的时候给val_添加引用计数,如下场景:
String a{"i am ref is one"}, c
String b(a)
c = b
我们知道a、b、c三个对象虽然是不同的String实例,但是他们属于同一个进程,可以共享堆内存或者说自由存储区,所以想要给val_添加引用计数,那么这个计数就需要放到堆空间,所以我们将val_进行如下改造:
struct StringValue {
int ref;
char* val;
StringValue(const char* v, int len): ref(0), val(nullptr) {
if (v && len > 0) {
val = new char[len + 1]();
std::copy_n(v, len, val);
}
}
~StringValue() { delete[] val; }
};
StringValue* val_ = nullptr;
我们可以看到val_类型被修改为StringValue,原来的char*被放到了StringValue结构体里,并添加了ref变量用来计数, 此时如果a、b、c三个val_相同,那么他们指向的val_->ref也是相同的,所以我们需要做点改动让a、b、c指向同一个val_
class String {
public:
~String() {
// 引用计数为0
if (val_ && --val_->ref) {
delete val_;
}
}
String(const char* val) : String(val, val ? strlen(val) : 0) {}
String(const char* val, int len) { val_ = new StringValue(val, len); }
String(const String& str) {
/*引用计数*/
this->val_ = str.val_; // 指向同一个val_
++(this->val_->ref); // 引用计数 +1
}
String& operator=(const String& str) {
/*引用计数*/
this->val_ = str.val_; // 指向同一个val_
++(this->val_->ref); // 引用计数 +1
return *this;
}
private:
struct StringValue {
int ref;
char* val;
StringValue(const char* v, int len): ref(0), val(nullptr) {
if (v && len > 0) {
val = new char[len + 1]();
std::copy_n(v, len, val);
}
}
~StringValue() { delete[] val; }
};
StringValue* val_ = nullptr;
};
我们可以看到,在拷贝构造和赋值操作的时候可以将其指向同一个堆内存。并且引用计数加一,当a、b、c对象析构的时候对引用计数减一,以此来达到对val的引用计数。
关于引用计数的计数原理已经讲完了,大家可以思考一个问题,为什么是对val进行引用计数,而不是使用static对class进行引用计数。