初始化类成员的有效方法;堆和堆栈都已分配

Efficient ways to initialize class members; both heap and stack allocated

本文关键字:堆栈 分配 成员 有效 方法 初始化      更新时间:2023-10-16

假设我们有以下内容:

//! SomeClass.hpp
class 
{
public:
    SomeClass( void );
   ~SomeClass( void ) 
    { 
       delete mFoo; 
       delete mBar; 
    }
    ...
private:
    Foo* mFoo;
    Bar* mBar;
    StackObj mStackFoo;
};
//! SomeClass.cpp
SomeClass::SomeClass( void )
{
     mFoo = new Foo;
     mBar = new Bar; 
     mStackFoo = StackObj( ... );
}

现在,当我们初始化指针时,我的理解是构造函数将创建一些不必要的SomeClass成员的副本,从而分配然后仅仅为了分配内存而释放内存。


通常使用初始化器列表,加上单独的初始化函数(用于堆分配内存)作为避免这种情况的方法。假设SomeClass有一个私有成员函数定义为void initHeapMem( void )。然后我们可以,

SomeClass::SomeClass( void )
    : mFoo( NULL ),
      mBar( NULL ),
      mStackFoo( ... )
{
     initHeapMem();
}
void SomeClass::initHeapMem( void )
{
    mFoo = new Foo;
    mBar = new Bar;
}

很自然地,这个在一定程度上解决了这个问题。我认为这里的问题是,仍然存在执行另一个函数调用的开销。

我们不能对原始指针使用初始化列表的原因是它们不是线程安全的。如果出现问题,程序抛出异常,仍然会有内存泄漏。注意:这是根据我所读到的,如果这是错误的,我的道歉


因此,在boost/c++ 11中,我们可以在头文件中使用#include <tr1/memory>指令中的智能指针(假设我们使用STL)。

如果我们使用std::unique_ptr< T >,那么我们将Bar* mBarFoo* mFoo替换为:

std::unique_ptr< Foo > mFoo;
std::unique_ptr< Bar > mBar;

这将允许我们做,

SomeClass::SomeClass( void )
   mFoo( new Foo ),
   mBar( new Bar ),
   mStackFoo( ... )
{
}

由于智能指针有效地的内存分配包装在自己的构造函数中。

虽然这是一个很好的解决方案,但我个人并不是一个为我创建的每个堆对象使用智能指针的人,我知道在c++社区中有其他人也有同样的感觉。


tl;博士

有了所有这些,我真正想知道的是是否有更有效的替代方法来初始化对象中的类成员(特别是随着c++ 11的出现),除了我上面列出的那些。

unique_ptr是正确的解决方案。它有几个优点:

  • 它明确地记录了所有权。原始指针的一个问题是,它们没有表明任何关于谁拥有它们的信息。对于unique_ptr,你有一个单一的所有者,如果你想转移它,必须明确地move所有权。

  • 它基本上没有开销;unique_ptr所做的唯一一件事就是调用删除器,无论如何您都要这样做。现在,您无需手动内存管理就可以获得确定性内存行为的性能优势。

  • 多亏了RAII,线程和异常安全更容易实现。这意味着更少的指令顺序和更少的显式清理代码。您可以获得异常的好处,而不会出现在c++ 03代码中导致避免异常的所有问题。

shared_ptr在我的经验中比unique_ptr需要的少得多。共享所有权主要在拥有不可变资源(如纹理或音频文件)时非常有用,因为这些资源的加载和复制都很昂贵,但您希望在不使用时卸载它们。shared_ptr还增加了安全性(特别是线程安全性)和引用计数的开销。

当然,缺点是智能指针增加了语法开销。它们不像原始指针那样"原生"。为此,您有typedefautodecltype和滚动您自己的方便函数,如make_unique

你为什么不能这么做?

SomeClass::SomeClass( void ) : 
mFoo(new Foo)
, mBar(new Bar)
{
}

它们是原始指针,不会创建不必要的副本。

我还应该指出,使用初始化列表的原因是,在执行构造函数体时,对象处于有效状态(即所有成员都具有有效值)。

SomeClass::SomeClass( void )
{
     //before this point, mFoo and mBar's values are unpredictable
     mFoo = new Foo;
     mBar = new Bar;
}

对于异常,只有在构造函数内部抛出异常时,才会调用SomeClass的析构函数。

最后,关于线程安全与否,它取决于每个线程是否有自己的someeclass副本,以及someeclass是否包含正在写入的静态成员。