5

只是一个设计/优化问题。你什么时候存储指针或对象,为什么?例如,我相信这两个工作(除非编译错误):

class A{
  std::unique_ptr<Object> object_ptr;
};

A::A():object_ptr(new Object()){}

class B{
  Object object;
};

B::B():object(Object()){}

我相信在堆栈或堆上实例化时会有一个区别?

例如:

   int main(){
      std::unique_ptr<A> a_ptr;
      std::unique_ptr<B> b_ptr;
      a_ptr = new A(); //(*object_ptr) on heap, (*a_ptr) on heap?
      b_ptr = new B(); //(*object_ptr) on heap, object on heap?

      A a;   //a on stack, (*object_ptr) on heap?
      B b;   //b on stack, object on stack?
}

另外,sizeof(A)应该是 < sizeof(B)? 还有其他我遗漏的问题吗?(丹尼尔在评论中的相关帖子中提醒了我关于继承问题)

因此,由于堆栈分配通常比堆分配快,但 A 的大小比 B 小,这些折衷是不是即使在使用移动语义的情况下如果不测试性能也无法回答的折衷方案之一?或者一些经验法则什么时候使用一个比另一个更有利?(San Jacinto 纠正了我关于堆栈/堆分配更快,而不是堆栈/堆)

我猜想更多的副本构造会导致相同的性能问题,(3 个副本将〜 3 倍类似的性能损失作为启动第一个实例)。但是移动构造可能更有利的是尽可能使用堆栈???

这是一个相关的问题,但不完全相同。 C++ STL:我应该存储整个对象还是指向对象的指针?

谢谢!

4

4 回答 4

6

如果你的内部有一个大对象A class,那么我会存储一个指向它的指针,但对于小对象或原始类型,在大多数情况下,你不应该真的需要存储指针。

此外,当某些东西存储在堆栈或堆(freestore)上时,实际上是依赖于实现的,A a并不总是保证在堆栈上。

最好将其称为自动对象,因为它的存储时间取决于声明它的函数的范围。当函数返回时,a将被销毁。

指针需要使用,new并且确实会带来一些开销,但是在今天的机器上,我想说它在大多数情况下都是微不足道的,除非您当然已经开始创建new数百万个对象,否则您将开始看到性能问题。

每种情况都不同,何时应该和不应该使用指针而不是自动对象,很大程度上取决于您的情况。

于 2011-09-13T17:08:37.120 回答
5

堆并不比堆栈“慢”。堆分配可能比堆栈分配慢,如果您以没有大量连续内存访问的方式设计对象和数据结构,那么糟糕的缓存局部性可能会导致大量缓存未命中。所以从这个角度来看,这取决于你的设计和代码使用目标是什么。

即使将这一点放在一边,您也必须质疑您的复制语义。如果您想要对象的深层副本(并且对象的对象也被深层复制),那么为什么还要存储指针呢?如果由于复制语义可以共享内存,则存储指针,但确保不要在 dtor 中两次释放内存。

我倾向于在两种情况下使用指针:类成员初始化顺序很重要,并且我将依赖项注入到对象中。在大多数其他情况下,我使用非指针类型。

编辑:当我使用指针时还有两种情况:1)避免循环包含依赖(尽管在某些情况下我可能会使用引用),2)使用多态函数调用的意图。

于 2011-09-13T17:12:13.457 回答
5

这取决于很多具体因素,任何一种方法都有其优点。我会说如果您将通过动态分配专门使用外部对象,那么您不妨让所有成员直接成员并避免额外的成员分配。另一方面,如果外部对象是自动分配的,大成员可能应该通过unique_ptr.

仅通过指针处理成员还有一个额外的好处:您删除了编译时依赖项,并且外部类的头文件可能能够摆脱内部类的前向声明,而不是要求完全包含内部类类的标题(“PIMPL”)。在大型项目中,这种脱钩可能在经济上是明智的。

于 2011-09-13T17:18:56.137 回答
2

在某些情况下,您几乎别无选择,只能存储指针。一个明显的情况是当您创建类似于二叉树的东西时:

template <class T>
struct tree_node { 
    struct tree_node *left, *right;
    T data;
}

在这种情况下,定义基本上是递归的,并且您预先不知道树节点可能有多少后代。您几乎被(至少是一些变体)存储指针所困扰,并根据需要分配后代节点。

还有一些情况,比如动态字符串,在父对象中只有一个对象(或对象数组),但它的大小可以在足够宽的范围内变化,您几乎需要(至少提供可能性)使用动态分配。对于字符串,小尺寸很常见,以至于有一个相当广泛使用的“短字符串优化”,其中字符串对象直接包含足够的空间用于字符串达到某个限制,以及一个指针以允许在字符串超过该限制时进行动态分配尺寸:

template <class T>
class some_string { 
    static const limit = 20;
    size_t allocated;
    size_t in_use;
    union { 
        T short_data[limit];
        T *long_data;
    };

    // ...

};

使用指针而不是直接存储子对象的一个​​不太明显的原因是为了异常安全。swap仅举一个明显的例子,如果您仅在父对象中存储指针,那么(通常确实)为那些提供保证的对象提供 a 变得微不足道nothrow

template <class T>
class parent { 
    T *data;

    void friend swap(parent &a, parent &b) throw() { 
         T *temp = a.data;
         a.data = b.data;
         b.data = temp;
    }
};

只有几个(通常是有效的)假设:

  1. 指针从一开始就有效,并且
  2. 分配有效的指针永远不会抛出异常

...这种交换nothrow无条件地提供保证是微不足道的(即,我们可以说:“交换不会抛出”)。如果parent直接存储对象而不是指针,我们只能有条件地保证(例如,swap当且仅当 T 的复制构造函数或赋值运算符抛出时才会抛出。”)

对于 C++11,经常(通常?)使用这样的指针可以很容易地提供一个非常有效的移动构造函数(这也提供了nothrow保证)。当然,使用指向(大部分)数据的指针并不是快速构建的唯一可能途径——但它很容易。

最后,我怀疑您在提出问题时想到了一些案例——所涉及的逻辑并不一定表明您应该使用自动分配还是动态分配。在这种情况下,它(显然)是一个判断电话。从纯理论的角度来看,在这些情况下使用它可能根本没有区别。然而,从实际的角度来看,它可以产生很大的不同。即使 C 和 C++ 标准都不能保证(甚至暗示)任何类似的东西,现实情况是,在大多数典型系统上,使用自动分配的对象最终会出现在堆栈上。在大多数典型系统(例如,Windows、Linux)上,堆栈仅限于可用内存的一小部分(通常在个位数到低两位数兆字节的数量级)。

这意味着如果在任何给定时间可能存在的所有这些类型的对象都可能超过几兆字节(左右),您需要确保(至少大部分)数据是动态分配的,而不是自动分配的。有两种方法可以做到这一点:您可以让用户在/如果它们可能超出可用堆栈空间时动态分配父对象,或者您可以让用户使用相对较小的“shell”对象来分配代表用户动态空间。

如果这很可能是一个问题,那么类处理动态分配而不是强迫用户这样做几乎总是更可取的。这有两个明显的好处:

  1. 用户可以使用基于堆栈的资源管理(SBRM,又名 RAII),并且
  2. 有限堆栈空间的影响是有限的,而不是“渗透”整个设计。

底线:特别是对于预先不知道存储类型的模板,我倾向于使用指针和动态分配。我将子对象的直接存储保留主要用于我知道存储类型(几乎?)总是很小的情况,或者分析表明动态分配导致真正的速度问题的情况。但是,在后一种情况下,我至少会operator new考虑为该类重载等替代方案。

于 2011-09-13T18:04:36.507 回答