【问题标题】:Class members that are objects - Pointers or not? C++作为对象的类成员 - 指针与否? C++
【发布时间】:2011-04-21 17:58:16
【问题描述】:

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

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

【问题讨论】:

  • 代码作为描述比英文好。

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


【解决方案1】:

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

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

就它在内存中的存储位置而言,它不是指针是什么意思?

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

创建类的时候会创建对象吗?

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

【讨论】:

  • 在大型项目中这样做的最大缺点是它强制声明 MyOtherClass 的标头的#include。这会很快导致编译时间非常慢。如果您使用(智能)指针,则可以使用前向声明。
  • @Ben +1 是的 - 我没有在我的帖子中提到模块间依赖关系和它们的抽象。这是在某些情况下支持动态分配成员的一个非常重要的原因。
  • 问题是在这种情况下如何在单元测试中模拟它?我正在使用 googlemock 框架,似乎用模拟对象替换成员对象的唯一方法是将其定义为指针....
【解决方案2】:

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

看看这个例子:

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类型的变量a_stack,那么该对象(显然)及其成员也在堆栈上。
  • 如果你像上面例子中的a_heap那样创建一个指向A的指针,那么只有指针变量在堆栈上;其他所有内容(对象及其成员)都在上。

B 的情况是什么样的:

  • 您在堆栈上创建B:那么对象及其成员foo都在堆栈上,但foo指向的对象(指针)在上。简而言之:b_stack.foo(指针)在堆栈上,但*b_stack.foo(指针)在堆上。
  • 您创建了一个指向B 的指针,名为b_heapb_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++, p。 99--119
  • 还有一些其他的,看看其他答案

建议

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

【讨论】:

  • 我不觉得从堆/堆栈的角度思考非常有用(特别是因为标准都没有真正定义)。我根据与包含块相关的寿命来考虑对象。具有作用域生命的对象应该是一个对象。具有动态生命周期的对象应该是指针(存储在智能指针中)。成员变量和函数变量之间的唯一区别是它们的作用域。成员变量的生命周期与其所在的对象的作用域相关。而函数变量与其函数(或块)的作用域相关。
  • 确实是这样,但问题是对象存储在内存中的什么位置,这对于在您的头脑中进行整理很有用。
  • 我发现这个评论比接受的答案更好。投票!
【解决方案3】:

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

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

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

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

【讨论】:

  • 另一方面,PIMPL 旨在通过在可见性中引入一层间接性来减少依赖关系。
  • pointee其实是一个字:)
【解决方案4】:

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

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

【讨论】:

    【解决方案5】:

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

    如果成员对象是给你的而不是你创建的,通常你会使用指针。那么你通常也不必销毁它。

    【讨论】:

      【解决方案6】:

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

      如果 MyOtherClass 不是指针:

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

      如果 MyOtherClass 是指针:

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

      【讨论】:

        【解决方案7】:

        指针成员的一些优点:

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

        将成员作为对象的优点:

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

        【讨论】:

          【解决方案8】:

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

          【讨论】:

            【解决方案9】:

            这取决于... :-)

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

             m_pA = new A();
            

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

            delete m_pA; 
            m_pA = NULL;
            

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

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

            • 如果您的对象分配在 堆栈和类型 A 使用大量内存 这不会从 堆栈,但来自堆。

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

            【讨论】:

              【解决方案10】:

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

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

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

              【讨论】:

                【解决方案11】:

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

                但是,在某些情况下仍需要指针。毕竟,托管语言(如 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 和智能指针(这里和网络上的其他地方有很多资源)。

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2017-03-09
                  • 1970-01-01
                  • 2021-06-06
                  • 1970-01-01
                  • 2012-07-07
                  相关资源
                  最近更新 更多