堆分配的成员引用是一个糟糕的想法吗?为什么

Are heap allocated member references a terrible idea? Why?

本文关键字:为什么 一个 引用 成员 分配      更新时间:2023-10-16

为了更好地解释这个问题,我构造了一个简单的示例。假设我有一个类Blob,如下所示:

class Blob
{
    string personalName;
    string& familyName;
}

Blob可以由Creator(又名程序员)生成,此时它可以选择personalName,并且由于它具有第一代Blob的特权,它可以选择自己的familyName

或者,可以通过衍生现有的Blob来创建Blob,此时它选择自己的personalName,但与该家族中克隆的所有其他Blob共享familyName。如果一个Blob更改了家族名称,则所有其他家族成员都会自动更改该名称。

到目前为止,这听起来都很好,直到在编写Blob构造函数时,我看到这样:
Blob::Blob() :
    personalName(pickName()),
    familyName(pickFamilyName())
{ }
...
string& Blob::pickFamilyName()
{
    return *(new string("George"));
}   // All Blobs have family name "George" in this example

唷!在堆上分配内存,然后将其分配给引用变量?!看起来很可怕!

我的直觉是正确的,这是非常错误的,还是只是因为它不是一个常见的模式而让我感到奇怪?如果有什么问题,是什么问题?为什么这是一个糟糕的设计?

注意:当最后一个Blob被销毁时,通过引用计数和删除内存来释放堆分配的内存是很重要的,或者通过其他方法。

将引用存储为类成员的唯一有意义的情况是:

  1. 数据不属于类,类不负责释放数据;
  2. 被引用对象的生存期保证比包含对象的生存期长。

我认为它的"臭味"部分是将变量存储为对字符串的引用,因为很难跟踪它是否为有效对象。为什么不使用如下格式:

boost::shared_ptr<std::string> Blob::pickFamilyName()
{
    return boost::shared_ptr<std::string>(new std::string("George"));
}

编辑

根据Praetorian的建议,你可以完全避免手动分配内存:

boost::shared_ptr<std::string> Blob::pickFamilyName()
{
    return boost::make_shared<std::string>("George");
}

一个参数将是一致性:operator new返回一个指针,而operator delete接受一个指针,因此预计用于引用动态分配对象的类型也将是指针,而不是引用。这是一个严肃的争论:如果你不一致并且毫无理由地违背程序员的习惯,你就会混淆他们并滋生bug。通常没有人会期望一个返回引用的函数在堆上创建新的对象,然后调用代码必须删除这些对象,所以迟早有人会忘记这样做。

但是也有一些实用的原因,指针可以被重新分配,它们可以被设置为null,这使得它们更方便处理动态分配的对象。在您的示例中,Blob类负责对引用成员调用delete。你通常会在析构函数中这样做。但是想象一下,你想更快地释放内存:对于指针,你可以在调用delete后给它们赋值null,然后让它们的析构函数安全地再次调用delete,对于引用成员,你就留下了一个悬空的引用,你不能做任何事情。

更严重的问题是异常安全:如果Blob具有较长的初始化列表或非空体,则构造函数可能在调用pickFamilyName()后抛出异常。在这种情况下,析构函数不会被调用,从而导致内存泄漏。理想情况下,您可以使用RAII来实现这一点,但是对于指针,也可以在初始化列表中将指针赋值为null,然后在try/catch块中将其指向构造函数体中新创建的对象,这样即使构造函数抛出并且没有析构函数调用,也可以确保对象被删除。这同样不能用引用来实现。