作为对象的类成员 - 指针与否? C ++

时间:2010-10-06 10:11:18

标签: c++ memory pointers class-design member

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

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

此致

标记

11 个答案:

答案 0 :(得分:30)

  

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

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

  

将它作为指针存储在内存中的位置也是什么意思呢?

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

  

创建类时是否会创建对象?

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

答案 1 :(得分:23)

您的会员在哪里存储在内存中?

看一下这个例子:

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!
} 

我们定义了两个类ABA存储foo类型的公开成员FooB的成员foo的类型为pointer to Foo

A的情况如何:

  • 如果您在堆栈上创建类型为a_stack的变量A,则该对象(显然)及其成员位于堆栈也是。
  • 如果你在上面的例子中创建了一个指向A的{​​{1}}指针,那么只有指针变量位于堆栈;其他一切(对象及其成员)都在

a_heap

的情况如何?
  • 您在堆栈上创建B:然后对象及其成员B都在堆栈上,但对象{ {1}}指向(指针对象)位于上。简而言之:foo(指针)在堆栈上,但foo(指针)在堆上。
  • 你创建了一个指向b_stack.foo *b_stack.foo的指针:B(指针)在堆栈上,b_heap(指针对象)在堆上,以及成员b_heap*b_heap

是否会自动创建对象?

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

所有这一切的重点是什么?

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

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

建议

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

答案 2 :(得分:17)

在C ++中,指针本身就是对象。他们并没有真正依赖于他们指向的东西,并且指针和它的指针之间没有特殊的交互(这是一个单词吗?)

如果创建指针,则创建指针而不创建其他。您不创建它可能或可能不指向的对象。当指针超出范围时,指向的对象不受影响。指针不会以任何方式影响它指向的生命周期。

因此,一般情况下,默认情况下应该使用指针。如果您的类包含另一个对象,则该另一个对象不应该是指针。

但是,如果您的类知道另一个对象,那么指针可能是表示它的好方法(因为您的类的多个实例可以指向同一个实例,而不需要拥有它,并且没有控制它的寿命)

答案 3 :(得分:6)

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

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

答案 4 :(得分:3)

这个问题可以无休止地审议,但基本要点是:

如果MyOtherClass不是指针:

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

如果MyOtherClass是指针:

  • MyOtherClass的创建和销毁是您的责任
  • MyOtherClass可能是NULL,这可能在您的上下文中有意义并且可以节省内存
  • 两个MyClass实例可以共享相同的MyOtherClass

答案 5 :(得分:3)

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

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

答案 6 :(得分:3)

指针成员的一些优点:

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

将成员作为对象的优点:

  • 您无需明确编写代码来创建和销毁对象。它更容易,也更不容易出错。
  • 提高内存管理效率,因为只需要分配一块内存而不是两块。
  • 实现赋值运算符,复制/移动构造函数等要简单得多。
  • 更直观

答案 7 :(得分: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保留为指针成员,因为它使您可以灵活地将其指向从其派生的任何其他类。基本上可以帮助您实现动态多态性。

答案 8 :(得分:1)

这取决于......: - )

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

 m_pA = new A();

此外,不要忘记销毁析构函数中的对象或者你有内存泄漏:

delete m_pA; 
m_pA = NULL;

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

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

  • 如果你的对象被分配了 堆栈和类型A使用大量内存 这不会从中分配 堆栈但是从堆中。

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

答案 9 :(得分:1)

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

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

当您使用auto_ptr时,您只需要处理构造,这通常可以在初始化列表中执行。 auto_ptr保证了与父对象的破坏。

答案 10 :(得分: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和智能指针(这里有很多资源和网络上的其他地方)。