50

如果我创建了一个 MyClass 类并且它有一些私有成员说 MyOtherClass,那么将 MyOtherClass 设为指针是否更好?就它在内存中的存储位置而言,将它作为不是指针意味着什么?创建类时会创建对象吗?

我注意到 QT 中的示例通常在类成员为类时将类成员声明为指针。

4

11 回答 11

35

如果我创建了一个 MyClass 类并且它有一些私有成员说 MyOtherClass,那么将 MyOtherClass 设为指针是否更好?

你通常应该在你的类中将它声明为一个值。它将是本地的,出错的机会更少,分配更少 - 最终可能出错的事情更少,并且编译器总是可以知道它在指定的偏移量处,所以......它有助于优化和二进制减少几个级别。在某些情况下,您知道必须处理指针(即多态、共享、需要重新分配),通常最好仅在必要时使用指针 - 特别是当它是私有/封装时。

就它在内存中的存储位置而言,将它作为不是指针意味着什么?

它的地址将接近(或等于)this——gcc(例如)有一些高级选项来转储类数据(大小、vtables、偏移量)

创建类时会创建对象吗?

是 - MyClass 的大小将增加 sizeof(MyOtherClass),或者如果编译器重新对齐它(例如到它的自然对齐),则更多

于 2010-10-06T10:35:07.467 回答
29

您的成员存储在内存中的什么位置?

看看这个例子:

struct Foo { int m; };
struct A {
  Foo foo;
};
struct B {
  Foo *foo;
  B() : foo(new Foo()) { } // ctor: allocate Foo on heap
  ~B() { delete foo; } // dtor: Don't forget this!
};

void bar() {
  A a_stack; // a_stack is on stack
             // a_stack.foo is on stack too
  A* a_heap = new A(); // a_heap is on stack (it's a pointer)
                       // *a_heap (the pointee) is on heap
                       // a_heap->foo is on heap
  B b_stack; // b_stack is on stack
             // b_stack.foo is on stack
             // *b_stack.foo is on heap
  B* b_heap = new B(); // b_heap is on stack
                       // *b_heap is on heap
                       // b_heap->foo is on heap
                       // *(b_heap->foo is on heap
  delete a_heap;
  delete b_heap;
  // B::~B() will delete b_heap->foo!
} 

我们定义了两个类AB. A存储foo类型的公共成员FooB有一个foo类型的成员pointer to Foo

什么情况A

  • 如果您在stack上创建a_stack类型变量,则该对象(显然)及其成员也在堆栈上。A
  • 如果您在上面的示例中创建一个指向Alike的指针a_heap,则只有指针变量在堆栈上;其他所有内容(对象及其成员)都在堆上

在以下情况下情况如何B

  • B堆栈上创建:然后对象及其成员foo都在堆栈上,但foo指向(指​​针)的对象在堆上。简而言之:(b_stack.foo指针)在堆栈上,但*b_stack.foo(指针)在堆上。
  • 您创建一个指向Bnamed的指针b_heap:(b_heap指针)在堆栈上,*b_heap(指针)在堆上,以及成员b_heap->foo*b_heap->foo.

对象会自动创建吗?

  • 在 A 的情况下:是的,foo将通过调用隐式默认构造函数来自动创建Foo. 这将创建一个integer不会初始化它(它将有一个随机数)!
  • 在 B 的情况下:如果您省略了我们的 ctor 和 dtor,那么foo(指针)也将被创建并用一个随机数初始化,这意味着它将指向堆上的一个随机位置。但请注意,指针存在!另请注意,隐式默认构造函数不会foo为您分配任何东西,您必须显式执行此操作。这就是为什么您通常需要显式构造函数和伴随的析构函数来分配和删除成员指针的指针。不要忘记复制语义:如果您复制对象(通过复制构造或赋值),指针会发生什么?

这一切的意义何在?

使用指向成员的指针有几个用例:

  • 指向您不拥有的对象。假设您的班级需要访问一个复制成本非常高的庞大数据结构。然后你可以保存一个指向这个数据结构的指针。请注意,在这种情况下,数据结构的创建删除超出了您的课程范围。必须有人照顾。
  • 增加编译时间,因为在您的头文件中不必定义指针。
  • 更高级一点;当你的类有一个指向另一个存储所有私有成员的类的指针时,“Pimpl idiom”:http ://c2.com/cgi/wiki?PimplIdiom ,也看看 Sutter, H. (2000): Exceptional C++,页。99--119
  • 还有一些,看看其他答案

建议

如果您的成员是指针并且您拥有它们,请格外小心。您必须编写适当的构造函数、析构函数并考虑复制构造函数和赋值运算符。如果复制对象,指针会发生什么?通常您还必须复制构造指针!

于 2010-10-06T11:05:42.810 回答
19

在 C++ 中,指针本身就是对象。它们并没有真正与它们指向的任何东西相关联,并且指针和它的指针之间没有特殊的交互(这是一个词吗?)

如果你创建一个指针,你创建一个指针,没有别的。您不会创建它可能指向或可能不指向的对象。当指针超出范围时,指向的对象不受影响。指针不会以任何方式影响它指向的任何东西的生命周期。

所以一般来说,你不应该默认使用指针。如果您的类包含另一个对象,则该其他对象不应该是指针。

但是,如果您的类知道另一个对象,那么指针可能是表示它的好方法(因为您的类的多个实例可以指向同一个实例,而无需获得它的所有权,也无需控制其生命周期)

于 2010-10-06T10:19:23.733 回答
8

C++ 中的常识是尽可能避免使用(裸)指针。尤其是指向动态分配内存的裸指针。

原因是指针使编写健壮的类变得更加困难,尤其是当您还必须考虑抛出异常的可能性时。

于 2010-10-06T10:23:55.343 回答
5

我遵循以下规则:如果成员对象与封装对象一起生存和死亡,则不要使用指针。如果成员对象由于某种原因必须比封装对象寿命长,您将需要一个指针。取决于手头的任务。

如果成员对象是给你的而不是你创建的,通常你会使用指针。然后你通常也不必破坏它。

于 2010-10-06T10:29:32.973 回答
4

这个问题可以无休止地考虑,但基础是:

如果 MyOtherClass 不是指针:

  • MyOtherClass 的创建和销毁是自动的,可以减少 bug。
  • MyOtherClass 使用的内存是 MyClassInstance 的本地内存,这可以提高性能。

如果 MyOtherClass 是一个指针:

  • MyOtherClass 的创建和销毁是你的责任
  • MyOtherClass 可能是NULL,这可能在您的上下文中有意义并且可以节省内存
  • MyClass 的两个实例可以共享同一个 MyOtherClass
于 2010-10-06T10:24:34.067 回答
4

指针成员的一些优点:

  • 子 (MyOtherClass) 对象可以具有与其父 (MyClass) 不同的生命周期。
  • 该对象可能在多个 MyClass(或其他)对象之间共享。
  • 编译 MyClass 的头文件时,编译器不一定要知道 MyOtherClass 的定义。您不必包含它的头文件,从而减少编译时间。
  • 使 MyClass 的大小更小。如果您的代码对 MyClass 对象进行大量复制,这对性能很重要。您可以只复制 MyOtherClass 指针并实现某种引用计数系统。

将成员作为对象的优点:

  • 您不必显式地编写代码来创建和销毁对象。它更容易,更不容易出错。
  • 使内存管理更有效,因为只需要分配一块内存而不是两块。
  • 实现赋值运算符、复制/移动构造函数等要简单得多。
  • 更直观
于 2010-10-06T11:01:39.540 回答
1

如果将 MyOtherClass 对象设为 MyClass 的成员:

size of MyClass = size of MyClass + size of MyOtherClass

如果将 MyOtherClass 对象设为 MyClass 的指针成员:

size of MyClass = size of MyClass + size of any pointer on your system

您可能希望将 MyOtherClass 保留为指针成员,因为它使您可以灵活地将其指向从它派生的任何其他类。基本上可以帮助您实现动态多态性。

于 2010-10-06T10:23:56.850 回答
1

这取决于... :-)

如果您使用指针说 a class A,则必须创建 A 类型的对象,例如在类的构造函数中

 m_pA = new A();

此外,不要忘记在析构函数中销毁对象,否则会出现内存泄漏:

delete m_pA; 
m_pA = NULL;

相反,在你的类中聚合一个类型为 A 的对象更容易,你不能忘记销毁它,因为这是在你的对象生命周期结束时自动完成的。

另一方面,拥有指针具有以下优点:

  • 如果您的对象是在堆栈上分配的,并且类型 A 使用大量内存,则不会从堆栈分配,而是从堆分配。

  • 您可以稍后构造您的 A 对象(例如在方法中Create)或提前销毁它(在方法中Close

于 2010-10-06T10:29:53.380 回答
1

父类将与成员对象的关系作为指向成员对象的 (std::auto_ptr) 指针来维护的一个优点是,您可以转发声明对象,而不必包含对象的头文件。

这在构建时解耦了类,允许修改成员对象的头类,而不会导致父类的所有客户端也被重新编译,即使它们可能不访问成员对象的函数。

当你使用 auto_ptr 时,你只需要处理构造,你通常可以在初始化列表中做。auto_ptr 保证与父对象一起销毁。

于 2010-10-06T11:26:46.603 回答
0

要做的简单的事情是将您的成员声明为对象。这样,您就不必关心复制构造、销毁和分配。这一切都是自动处理的。

但是,在某些情况下,您仍然需要指针。毕竟,托管语言(如 C# 或 Java)实际上是通过指针保存成员对象。

最明显的情况是要保留的对象是多态的。正如您所指出的,在 Qt 中,大多数对象都属于多态类的巨大层次结构,并且通过指针保存它们是强制性的,因为您事先不知道成员对象的大小。

请注意这种情况下的一些常见陷阱,尤其是在处理泛型类时。异常安全是一个大问题:

struct Foo
{
    Foo() 
    {
        bar_ = new Bar();
        baz_ = new Baz(); // If this line throw, bar_ is never reclaimed
                          // See copy constructor for a workaround
    }

    Foo(Foo const& x)
    {
        bar_ = x.bar_.clone();
        try { baz_ = x.baz_.clone(); }
        catch (...) { delete bar_; throw; }
    }

    // Copy and swap idiom is perfect for this.
    // It yields exception safe operator= if the copy constructor
    // is exception safe.
    void swap(Foo& x) throw()
    { std::swap(bar_, x.bar_); std::swap(baz_, x.baz_); }

    Foo& operator=(Foo x) { x.swap(*this); return *this; }

private:
    Bar* bar_;
    Baz* baz_;
};

如您所见,在存在指针的情况下拥有异常安全的构造函数非常麻烦。您应该查看 RAII 和智能指针(这里和网络上的其他地方有很多资源)。

于 2010-10-06T11:49:30.957 回答