簡體   English   中英

作為對象的類成員 - 指針與否? C++

[英]Class members that are objects - Pointers or not? C++

如果我創建一個類 MyClass 並且它有一些私有成員說 MyOtherClass,那么將 MyOtherClass 設為指針是否更好? 就它在內存中的存儲位置而言,讓它不是指針又意味着什么? 創建類時會創建對象嗎?

我注意到 QT 中的示例通常在類成員是類時將類成員聲明為指針。

如果我創建一個類 MyClass 並且它有一些私有成員說 MyOtherClass,那么將 MyOtherClass 設為指針是否更好?

你通常應該在你的類中將它聲明為一個值。 它將是本地的,出錯的機會更少,分配更少 - 最終可能出錯的事情更少,並且編譯器總是可以知道它在指定的偏移量處,所以......它有助於優化和二進制減少幾個級別。 在某些情況下,您知道必須處理指針(即多態、共享、需要重新分配),通常最好僅在必要時使用指針 - 特別是當它是私有/封裝時。

就它在內存中的存儲位置而言,讓它不是指針又意味着什么?

它的地址將接近(或等於) this ——gcc(例如)有一些高級選項來轉儲類數據(大小、虛擬表、偏移量)

創建類時會創建對象嗎?

是 - MyClass 的大小將增加 sizeof(MyOtherClass),或者更多,如果編譯器重新對齊它(例如到它的自然對齊)

您的成員存儲在內存中的什么位置?

看看這個例子:

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類型的公共成員foo B有一個pointer to Foo類型的成員foo

A的情況是什么:

  • 如果您在stack上創建類型為A的變量a_stack ,則該對象(顯然)及其成員也在堆棧上
  • 如果你在上面的例子中像a_heap一樣創建一個指向A的指針,那么只有指針變量在堆棧上 其他所有內容(對象及其成員)都在堆上

B情況下情況如何:

  • 您在堆棧上創建B :然后對象及其成員foo都在堆棧上,但foo指向的對象(指針對象)在堆上 簡而言之: b_stack.foo (指針)在棧上,但*b_stack.foo (指針)在堆上。
  • 您創建了一個名為b_heap的指向B的指針: b_heap (指針)在堆棧上, *b_heap (指針)在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
  • 還有一些,看看其他答案

忠告

如果您的成員是指針並且您擁有它們,請格外小心。 您必須編寫適當的構造函數、析構函數並考慮復制構造函數和賦值運算符。 如果復制對象,指針對象會發生什么? 通常,您還必須復制構造指針!

在 C++ 中,指針本身就是對象。 它們並沒有真正綁定到它們指向的任何東西,並且指針和它的被指點對象之間沒有特殊的交互(這是一個詞嗎?)

如果你創建一個指針,你就創建了一個指針而不是其他任何東西 您不會創建它可能指向或不指向的對象。 當指針超出范圍時,指向的對象不受影響。 指針不會以任何方式影響它指向的任何東西的生命周期。

因此,在一般情況下,你應該使用默認的指針。 如果您的類包含另一個對象,則該其他對象不應是指針。

但是,如果您的類知道另一個對象,那么指針可能是表示它的好方法(因為您的類的多個實例可以指向同一個實例,無需取得它的所有權,也無需控制其生命周期)

C++ 中的常識是盡可能避免使用(裸)指針。 特別是指向動態分配內存的裸指針。

原因是因為指針使得編寫健壯的類變得更加困難,尤其是當您還必須考慮拋出異常的可能性時。

我遵循以下規則:如果成員對象與封裝對象一起生存和死亡,則不要使用指針。 如果成員對象由於某種原因必須比封裝對象存活時間更長,則您將需要一個指針。 取決於手頭的任務。

如果成員對象是給你的而不是你創建的,通常你會使用指針。 那么你通常也不必銷毀它。

這個問題可以無休止地討論,但基礎是:

如果 MyOtherClass 不是指針:

  • MyOtherClass 的創建和銷毀是自動的,可以減少 bug。
  • MyOtherClass 使用的內存是 MyClassInstance 的本地內存,這可以提高性能。

如果 MyOtherClass 是一個指針:

  • MyOtherClass 的創建和銷毀是你的責任
  • MyOtherClass 可能是NULL ,這在您的上下文中可能有意義並且可以節省內存
  • MyClass 的兩個實例可以共享同一個 MyOtherClass

指針成員的一些優點:

  • 子對象 (MyOtherClass) 可以具有與其父對象 (MyClass) 不同的生命周期。
  • 該對象可能會在多個 MyClass(或其他)對象之間共享。
  • 在為 MyClass 編譯頭文件時,編譯器不一定要知道 MyOtherClass 的定義。 您不必包含其標頭,從而減少編譯時間。
  • 使 MyClass 大小更小。 如果您的代碼對 MyClass 對象進行了大量復制,這對於性能來說可能很重要。 您可以只復制 MyOtherClass 指針並實現某種引用計數系統。

將成員作為對象的優點:

  • 您不必明確地編寫代碼來創建和銷毀對象。 它更容易,也更不容易出錯。
  • 使內存管理更高效,因為只需要分配一塊內存而不是兩塊。
  • 實現賦值運算符、復制/移動構造函數等要簡單得多。
  • 更直觀

如果您將 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 作為指針成員保留,因為它使您可以靈活地將其指向從它派生的任何其他類。 基本上可以幫助您實現動態多態性。

這取決於... :-)

如果您使用指針表示class A ,則必須在類的構造函數中創建類型 A 的對象,例如

 m_pA = new A();

此外,不要忘記在析構函數中銷毀對象,否則會出現內存泄漏:

delete m_pA; 
m_pA = NULL;

相反,在您的類中聚合類型為 A 的對象更容易,您不能忘記銷毀它,因為這會在您的對象生命周期結束時自動完成。

另一方面,擁有指針具有以下優點:

  • 如果您的對象在堆棧上分配並且類型 A 使用大量內存,則不會從堆棧分配而是從堆分配。

  • 您可以稍后構造您的 A 對象(例如在方法Create )或提前銷毀它(在方法Close

父類將與成員對象的關系作為指向成員對象的 (std::auto_ptr) 指針來維護的一個優點是,您可以向前聲明該對象,而不必包含該對象的頭文件。

這在構建時解耦了類,允許修改成員對象的頭類,而不會導致父類的所有客戶端也被重新編譯,即使它們可能不訪問成員對象的函數。

當您使用 auto_ptr 時,您只需要注意構造,這通常可以在初始化列表中完成。 auto_ptr 保證與父對象一起銷毀。

要做的簡單的事情是將您的成員聲明為對象。 這樣,您就不必關心復制構造、銷毀和賦值。 這一切都是自動處理的。

但是,仍有一些情況需要指針。 畢竟,托管語言(如 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 和智能指針(這里和網絡上的其他地方有很多資源)。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM