Члены класса, которые являются объектами-указателями или нет? С++
Если я создаю класс MyClass и у него есть какой-то частный член, скажем MyOtherClass, лучше ли сделать MyOtherClass указателем или нет? Что значит также иметь его как не указатель с точки зрения того, где он хранится в памяти? Будет ли объект создан при создании класса?
Я заметил, что примеры в QT обычно объявляют членов класса указателями, когда они являются классами.
в отношении
Марк
11 ответов
Если я создаю класс MyClass и у него есть какой-то частный член, скажем MyOtherClass, лучше ли сделать MyOtherClass указателем или нет?
обычно вы должны объявить его как значение в своем классе. он будет локальным, будет меньше шансов на ошибки, меньше распределений-в конечном счете меньше вещей, которые могут пойти не так, и компилятор всегда может знать, что он есть при заданном смещении... это помогает оптимизации и двоичного сокращения на нескольких уровнях. там будет несколько случаев, когда вы знаете, что вам придется иметь дело с указателем (т. е. полиморфным, общим, требует перераспределения), обычно лучше использовать указатель только при необходимости - особенно когда он является частным/инкапсулированным.
что означает также иметь его как не указатель с точки зрения того, где он хранится в памяти?
его адрес будет близок к (или равен) this
-- gcc (например) имеет некоторые дополнительные параметры для дампа данных класса (размеры, vtables, offsets)
будет ли объект создан при создании класса?
yes-размер 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!
}
определим два класса A
и B
. A
сохраняет публичного участника foo
типа Foo
. B
есть типа pointer to Foo
.
какова ситуация для A
:
- если вы создаете переменную
a_stack
типаA
на стек, тогда объект (очевидно) и его члены находятся на the стек тоже. - если вы создадите указатель на
A
какa_heap
в приведенном выше примере только переменная указателя находится на стек; все остальное (объект и его члены) находятся на кучу.
как выглядит ситуация в случае B
:
- создать
B
на стек: потом и объект, и его членfoo
на стек, но объект, чтоfoo
указывает на (pointee) находится на кучу. Короче:b_stack.foo
(указатель) находится в стеке, но*b_stack.foo
(pointee)находится в куче. - вы создаете указатель на
B
имениb_heap
:b_heap
(указатель) находится в стеке,*b_heap
(том ссылающиеся на заданный) на кучу, а также и*b_heap->foo
.
будет ли объект автоматически создан?
- в случае A: да,
foo
будет автоматически создан путем вызова неявного конструктора по умолчаниюFoo
. Это создастinteger
но не intitialize его (он будет иметь случайное число)! - в случае B: если вы опустите наш ctor и dtor, то
foo
(указатель) также будет создан и инициализирован случайным числом, что означает, что он будет указывать на случайном месте в куче. Но обратите внимание, что указатель существует! Обратите внимание также, что неявный конструктор по умолчанию не будет выделять что-то дляfoo
для вас, вы должны сделать это явно. Вот почему вам обычно нужен явный конструктор и сопровождающих деструктор для выделения и удаления указателя вашего члена. Не забывайте о скопировать семантику: что произойдет с указателем, если вы скопируете объект (через конструкцию копирования или задание)?
в чем смысл всего этого?
существует несколько случаев использования указателя на член:
- чтобы указать на объект, которым вы не владеете. Предположим, вашему классу нужен доступ к огромной структуре данных, которую очень дорого копировать. Затем вы можете просто сохранить указатель на эту структуру данных. Имейте в виду, что в этом случае создание и удаление структуры данных выходит за рамки ваш класс. Кто-то другой должен позаботиться.
- увеличение времени компиляции, так как в вашем заголовочном файле не обязательно указывать указатель.
- немного более продвинутый; когда ваш класс имеет указатель на другой класс, который хранит все частные члены, "идиома Pimpl":http://c2.com/cgi/wiki?PimplIdiom, взгляните также на Саттера, H. (2000): Исключительный C++, p. 99--119
- и некоторые другие, посмотрите на другие ответы
советы
будьте осторожны, если ваши члены являются указателями, и вы владеете ими. Вы должны написать правильные конструкторы, деструкторы и подумать о конструкторах копирования и операторах назначения. Что произойдет с указателем, если вы скопируете объект? Обычно вам также придется копировать конструкцию pointee!
в C++ указатели являются объектами сами по себе. Они на самом деле не привязаны к тому, на что они указывают, и нет никакого специального взаимодействия между указателем и его указателем (это слово?)
Если вы создаете указатель, вы создаете указатель и больше ничего. Вы не создаете объект, на который он может указывать или не указывать. И когда указатель выходит из области видимости, объект, на который указывают, не изменяется. Указатель никоим образом не влияет на время жизни того, что он очков.
Так, в общем, вы должны не по умолчанию используйте указатели. Если ваш класс содержит другой объект, другой объект не должен быть указателем.
однако, если ваш класс знает о другой объект, тогда указатель может быть хорошим способом его представления (так как несколько экземпляров вашего класса могут указывать на один и тот же экземпляр, не принимая на себя ответственность за него и не контролируя его время жизни)
общая мудрость в C++ заключается в том, чтобы избегать использования (голых) указателей как можно больше. Особенно голые указатели, которые указывают на динамически выделенную память.
причина в том, что указатели затрудняют написание надежных классов, особенно когда вам также приходится учитывать возможность возникновения исключений.
этот вопрос можно было бы обсуждать бесконечно, но основы таковы:
если MyOtherClass не является указателем:
- создание и уничтожение MyOtherClass происходит автоматически, что может уменьшить количество ошибок.
- память, используемая 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 в качестве члена указателя, потому что это дает вам гибкость, чтобы указать его на любой другой класс, производный от него. В основном помогает реализовать полиморфизм dynamice.
Это зависит... :-)
если вы используете указатели, чтобы сказать class A
, вы должны создать объект типа A, например, в конструкторе вашего класса
m_pA = new A();
не забудьте уничтожить объект в деструкторе или у вас есть утечка памяти:
delete m_pA;
m_pA = NULL;
вместо этого, имея объект типа 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 и умные указатели (есть много ресурсы здесь и где-то еще в интернете).