为了最好地解释这个问题,我构建了一个简单的例子。假设我有一个课程Blob
,如下所示:
class Blob
{
string personalName;
string& familyName;
}
创建者(又名程序员)可以生成Blob
,此时它可以选择personalName
,因为它有权成为第一代Blob
,它可以选择它自己的familyName
。
或者,可以通过生成现有的Blob
来创建Blob
,此时它会选择自己的personalName
,但与所有其他familyName
共享Blob
{1}}已被克隆在这个家庭中。如果一个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被破坏时删除内存或通过其他方法释放内存非常重要。
答案 0 :(得分:14)
将引用存储为类成员的唯一时间是:
您的示例违反了规则1.
答案 1 :(得分:8)
我认为它的“臭”部分是将变量存储为字符串的引用,因为很难跟踪它是否是一个有效的对象。为什么不使用类似的东西:
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");
}
答案 2 :(得分:1)
一个参数是一致性:operator new
返回一个指针,operator delete
接受指针,因此预期用于引用动态分配的对象的类型也将是指针,而不是引用。这是一个严肃的论点:如果你是不一致的并且没有充分理由反对程序员的习惯,那么你就会混淆它们并滋生错误。通常没有人会期望一个函数返回一个引用来在堆上创建新对象,然后调用代码必须删除它,所以迟早会有人忘记这样做。
但是还有一些实用的原因,指针可以重新分配,并且可以将它们设置为null,这使得它们更便于处理动态分配的对象。在您的示例中,Blob类负责在引用成员上调用delete。您通常会在析构函数中执行此操作。但是想象一下你想要更快地释放内存:使用指针你可以在调用delete之后为它们分配null,然后让它们的析构函数再次安全地调用delete,使用引用成员你会留下一个你不能做的悬空引用任何事情。
更严重的问题是异常安全:如果Blob具有更长的初始化列表或非空体,则构造函数可能在调用pickFamilyName()之后抛出。在这种情况下,析构函数不会被调用,并且您有内存泄漏。理想情况下,您可以使用RAII,但是使用指针也可以在初始化列表中将指针指定为null,然后将其指向try / catch块中构造函数体中新创建的对象,以确保对象即使构造函数抛出并且没有析构函数调用,也会被删除。这也不能用参考文献来完成。