Члены класса, которые являются объектами - указатели или нет? С++

Если я создаю класс MyClass, и у него есть какой-то закрытый член, скажем, MyOtherClass, лучше ли сделать MyOtherClass указателем или нет? Что значит также иметь его как не указатель с точки зрения того, где он хранится в памяти? Будет ли объект создан при создании класса?

Я заметил, что примеры в QT обычно объявляют члены класса как указатели, когда они являются классами.


person Mark    schedule 06.10.2010    source источник
comment
Код лучше английского в качестве описания.   -  person Martin York    schedule 06.10.2010


Ответы (11)


Если я создаю класс MyClass, и у него есть какой-то закрытый член, скажем, MyOtherClass, лучше ли сделать MyOtherClass указателем или нет?

обычно вы должны объявить его как значение в своем классе. он будет локальным, будет меньше шансов на ошибки, меньше выделений - в конечном итоге меньше вещей, которые могут пойти не так, и компилятор всегда может знать, что он есть по указанному смещению, поэтому... это помогает оптимизации и двоичному сокращению в несколько уровней. будет несколько случаев, когда вы знаете, что вам придется иметь дело с указателем (т. е. полиморфным, общим, требующим перераспределения), обычно лучше использовать указатель только при необходимости, особенно когда он является закрытым/инкапсулированным.

Что значит также иметь его как не указатель с точки зрения того, где он хранится в памяти?

его адрес будет близок (или равен) this -- gcc (например) имеет некоторые дополнительные параметры для вывода данных класса (размеры, виртуальные таблицы, смещения)

Будет ли объект создан при создании класса?

да - размер MyClass будет увеличиваться на sizeof(MyOtherClass) или больше, если компилятор выровняет его (например, до его естественного выравнивания)

person justin    schedule 06.10.2010
comment
Большим недостатком этого в больших проектах является принудительное #include заголовка, в котором объявлен MyOtherClass. Это может быстро привести к очень медленному времени компиляции. Если вы используете (умный) указатель, вам может сойти с рук предварительное объявление. - person Ben; 07.10.2010
comment
@Ben +1 да - я не упомянул межмодульные зависимости и их абстракцию в своем посте. это очень важная причина в некоторых случаях отдавать предпочтение динамически выделяемым членам. - person justin; 08.10.2010
comment
Проблема в том, как я могу издеваться над ним в модульном тесте в этом случае? Я использую структуру googlemock, кажется, единственный способ заменить объект-член фиктивным объектом - это если он определен как указатель.... - person Superziyi; 31.12.2020

Где в памяти хранится ваш член?

Взгляните на этот пример:

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

Мы определяем два класса A и B. A хранит открытый член foo типа Foo. B имеет элемент foo типа pointer to Foo.

Как обстоят дела с A:

  • Если вы создаете переменную a_stack типа A в стеке, то объект (очевидно) и его элементы также находятся в стеке.
  • Если вы создадите указатель на A, как a_heap в приведенном выше примере, в стеке будет только переменная указателя; все остальное (объект и его элементы) находится в куче.

Как выглядит ситуация в случае B:

  • вы создаете B в стеке: тогда и объект, и его элемент foo находятся в стеке, но объект, на который указывает foo (указатель), находится в стеке >куча. Короче говоря: b_stack.foo (указатель) находится в стеке, но *b_stack.foo (указатель) находится в куче.
  • вы создаете указатель на B с именем b_heap: b_heap (указатель) находится в стеке, *b_heap (указатель) находится в куче, а также элементы b_heap->foo и *b_heap->foo.

Будет ли объект создан автоматически?

  • В случае A: Да, foo будет автоматически создан путем вызова неявного конструктора по умолчанию Foo. Это создаст integer, но не инициализирует его (у него будет случайное число)!
  • В случае B: если вы опустите наши ctor и dtor, тогда foo (указатель) также будет создан и инициализирован случайным числом, что означает, что он будет указывать на случайное местоположение в куче. Но обратите внимание, что указатель существует! Также обратите внимание, что неявный конструктор по умолчанию не будет выделять что-либо для foo за вас, вы должны сделать это явно. Вот почему вам обычно нужен явный конструктор и сопутствующий деструктор для выделения и удаления указателя вашего указателя-члена. Не забывайте о семантике копирования: что произойдет с указателем, если вы скопируете объект (с помощью конструкции копирования или присваивания)?

Какой смысл во всем этом?

Существует несколько вариантов использования указателя на член:

  • Чтобы указать на объект, которым вы не владеете. Допустим, вашему классу нужен доступ к огромной структуре данных, копирование которой обходится очень дорого. Тогда вы можете просто сохранить указатель на эту структуру данных. Имейте в виду, что в этом случае создание и удаление структуры данных выходит за рамки вашего класса. Кто-то другой должен заботиться.
  • Увеличение времени компиляции, так как в вашем заголовочном файле указатель не должен быть определен.
  • Немного более продвинутый; Когда в вашем классе есть указатель на другой класс, в котором хранятся все частные члены, используется «идиома Pimpl»: http://c2.com/cgi/wiki?PimplIdiom, см. также Sutter, H. (2000): Exceptional C++, p. 99--119
  • И некоторые другие, посмотрите на другие ответы

Совет

Будьте особенно осторожны, если ваши члены являются указателями, и вы владеете ими. Вы должны написать правильные конструкторы, деструкторы и подумать о конструкторах копирования и операторах присваивания. Что произойдет с указателем, если вы скопируете объект? Обычно вам также придется копировать конструкцию указателя!

person WolfgangP    schedule 06.10.2010
comment
Я не считаю, что мышление с точки зрения кучи/стека очень полезно (тем более, что ни то, ни другое не определено стандартом). Я думаю об объектах с точки зрения их продолжительности жизни по отношению к содержащему блоку. Объект с ограниченным сроком службы должен быть объектом. Объект с динамическим сроком службы должен быть указателем (хранящимся в интеллектуальном указателе). Единственное различие между переменной-членом и функциональной переменной заключается в их области действия. Продолжительность жизни переменных-членов зависит от их области действия, объекта, в котором они находятся. В то время как функциональные переменные относятся к области действия функции (или блока). - person Martin York; 06.10.2010
comment
Это определенно так, но вопрос заключался в том, где объекты хранятся в памяти, что полезно для сортировки вещей в вашей голове. - person WolfgangP; 06.10.2010
comment
Я нашел этот комментарий лучше, чем принятый ответ. Голосуйте за! - person Kazooie; 10.10.2019

В C++ указатели являются самостоятельными объектами. На самом деле они не привязаны к тому, на что указывают, и нет никакого особого взаимодействия между указателем и его указателем (это слово?)

Если вы создаете указатель, вы создаете указатель и ничего больше. Вы не создаете объект, на который он может указывать или не указывать. И когда указатель выходит за пределы области видимости, объект, на который он указывает, не затрагивается. Указатель никоим образом не влияет на время жизни того, на что он указывает.

Таким образом, по умолчанию вы не должны использовать указатели. Если ваш класс содержит другой объект, этот другой объект не должен быть указателем.

Однако, если ваш класс знает о другом объекте, то указатель может быть хорошим способом его представления (поскольку несколько экземпляров вашего класса могут указывать на один и тот же экземпляр, не становясь его владельцем, и не контролируя его срок службы)

person jalf    schedule 06.10.2010
comment
С другой стороны, PIMPL направлен на сокращение зависимостей путем введения слоя косвенности в видимость. - person Matthieu M.; 06.10.2010
comment
указатель на самом деле слово :) - person Ayxan Haqverdili; 30.08.2019

Здравый смысл в C++ состоит в том, чтобы максимально избегать использования (голых) указателей. Особенно голые указатели, указывающие на динамически выделенную память.

Причина в том, что указатели усложняют написание надежных классов, особенно когда вам также необходимо учитывать возможность возникновения исключений.

person Bart van Ingen Schenau    schedule 06.10.2010

Я следую следующему правилу: если объект-член живет и умирает вместе с инкапсулирующим объектом, не используйте указатели. Вам понадобится указатель, если объект-член по какой-то причине должен пережить инкапсулирующий объект. Зависит от поставленной задачи.

Обычно вы используете указатель, если объект-член предоставлен вам, а не создан вами. Тогда вам обычно не нужно его уничтожать.

person chvor    schedule 06.10.2010

Этот вопрос можно обсуждать бесконечно, но суть такова:

Если MyOtherClass не является указателем:

  • Создание и уничтожение MyOtherClass происходит автоматически, что может уменьшить количество ошибок.
  • Память, используемая MyOtherClass, является локальной для MyClassInstance, что может повысить производительность.

Если MyOtherClass является указателем:

  • Вы несете ответственность за создание и уничтожение MyOtherClass.
  • MyOtherClass может быть NULL, что может иметь значение в вашем контексте и может сэкономить память.
  • Два экземпляра MyClass могут использовать один и тот же MyOtherClass.
person Drew Dormann    schedule 06.10.2010

Некоторые преимущества члена указателя:

  • Дочерний объект (MyOtherClass) может иметь другое время жизни, чем его родительский (MyClass).
  • Объект может быть разделен между несколькими объектами MyClass (или другими).
  • При компиляции заголовочного файла для MyClass компилятору не обязательно знать определение MyOtherClass. Вам не нужно включать его заголовок, что сокращает время компиляции.
  • Уменьшает размер MyClass. Это может быть важно для производительности, если ваш код много копирует объекты MyClass. Вы можете просто скопировать указатель MyOtherClass и реализовать какую-то систему подсчета ссылок.

Преимущества наличия члена в качестве объекта:

  • Вам не нужно явно писать код для создания и уничтожения объекта. Это проще и менее подвержено ошибкам.
  • Делает управление памятью более эффективным, поскольку необходимо выделить только один блок памяти вместо двух.
  • Реализация операторов присваивания, конструкторов копирования/перемещения и т. д. намного проще.
  • Более интуитивно понятный
person Timo    schedule 06.10.2010

Если вы сделаете объект 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 в качестве члена-указателя, потому что это дает вам возможность указывать его на любой другой класс, производный от него. В основном помогает вам реализовать динамический полиморфизм.

person Alok Save    schedule 06.10.2010

Это зависит... :-)

Если вы используете указатели, чтобы сказать class A, вам нужно создать объект типа A, например. в конструкторе вашего класса

 m_pA = new A();

Кроме того, не забудьте уничтожить объект в деструкторе, иначе у вас будет утечка памяти:

delete m_pA; 
m_pA = NULL;

Вместо этого проще иметь объект типа A, агрегированный в вашем классе, вы не можете забыть его уничтожить, потому что это делается автоматически в конце жизни вашего объекта.

С другой стороны, наличие указателя имеет следующие преимущества:

  • Если ваш объект выделен в стеке, а тип A использует много памяти, он будет выделен не из стека, а из кучи.

  • Вы можете построить свой объект A позже (например, в методе Create) или уничтожить его раньше (в методе Close)

person ur.    schedule 06.10.2010

Преимущество родительского класса, поддерживающего отношение к объекту-члену в виде указателя (std::auto_ptr) на объект-член, заключается в том, что вы можете предварительно объявить объект, а не включать заголовочный файл объекта.

Это разделяет классы во время сборки, позволяя изменять класс заголовка объекта-члена, не вызывая перекомпиляции всех клиентов вашего родительского класса, даже если они, вероятно, не имеют доступа к функциям объекта-члена.

Когда вы используете auto_ptr, вам нужно только позаботиться о построении, что вы обычно делаете в списке инициализаторов. Уничтожение вместе с родительским объектом гарантируется auto_ptr.

person andreas buykx    schedule 06.10.2010

Самое простое — объявить членов как объекты. Таким образом, вам не нужно заботиться о создании, уничтожении и присвоении копий. Обо всем этом позаботятся автоматически.

Однако бывают случаи, когда вам нужны указатели. В конце концов, управляемые языки (такие как 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 и интеллектуальные указатели (есть много ресурсов здесь и где-то еще в Интернете).

person Alexandre C.    schedule 06.10.2010