1. Подобъекты. Отношения включения между объектами Пусть определен такой класс: class A { B b; C c; ... }; ... A a; Говорят, что a включает b и c, или a состоит из b и c, или b и c являются частью a. (?) В какой момент вызываются конструкторы подобъектов? деструкторы подобъектов? Список инициализации Пусть у нас есть классы: class B {... B(int); ...}; class A { B b; C c; int n; // (1) (2) public: // ↓ __↓__ A(int bi, const C& cc, int nn) : b(bi), c(cc) {n = nn;} ... // -----------}; // Список иниц. К моменту (1) уже отведена память под данные-члены объекта основного класса и его подобъектов. Указатель this также создан. Меткой (2) отмечен вызов конструктора копий. Конструкторы подобъектов должны быть вызваны до конструктора основного объекта („в прологе“ конструктора основного объекта) в специальной области, называемой списком инициализации. Но до их вызова должна быть выделена память под все данные члены объекта основного класса, а, следовательно, и под все данные-члены подобъектов: a b c n Таким образом, размер подобъектов к этому моменту должен быть известен, следовательно классы подобъектов должны быть описаны до класса основного объекта. (!) [ЦВВ] Порядок инициализации данных-членов не завистит от порядка их перечисления в списке инициализации, а зависит только от порядка их объявления внутри класса. Старайтесь писать код, не зависящий от порядка инициализации. Если вызов конструктора для подобъекта в списке инициализации отсутствует, то вызывается конструктор по умолчанию. Если его в классе подобъекта нет, то происходит ошибка времени компиляции. (П) Пример скрытой ошибки // вызывается operator= // ↓ A(const B& bb) {b = bb;} // ↑ // вызывается конструк тор B по умолчанию Деструкторы подобъектов вызываются в порядке, обратном вызову конструкторов, т. е. в начале вызывается деструктор основного объекта, а после него („в эпилоге“ деструктора основного объекта) — деструкторы подобъектов. Если деструктор у основного объекта отсутствует, то генерируется деструктор по умолчанию, и он вызывает деструкторы подобъектов. (П) Порядок вызова конструкторов NIY Сюда нужно вставить рисунок. Деструкторы — в обратном порядке. Если у основного объекта отсутствуют конструкторы, то генерируется конструктор по умолчанию, который вызывает конструкторы по умолчанию для подобъектов. (!) Замечание о копирующем конструкторе Если конструктор копий не определён, то генерируется предопределенный конструктор копий, который поэлементно копирует данные-члены. Если данное-член — объект некоторого класса, то для его копирования вызывается его конструктор копий. Т. о., в ситуации class A { B b; C c; int n; ... }; конструктор копий можно не писать, а в ситуации class A { B b; C c; int* data; // данные выделяются в динамической памяти ... A(const A& a) : b(a.b), c(a.c) {data = new int; ... } }; писать обязательно. В этом случае предпочтительно int* data „обернуть“ классом с простым конструктором копий, тогда конструктор еопий для класса A не надо будет писать (равно как и operator=). (?) Что можно и что обязательно инициализировать в списке инициализации? Обязательно нужно инициализировать: • объектные данные-члены; • ссылочные данные-члены; • константные данные-члены. (П) class A { const int i; int n; B& b; // ссылка и константа обязательно должны иниц- ся при создании public: // ↓ ↓ A(int ii, B& bb, int nn) : i(ii), b(bb), n(nn) {} ... // ------------ ----}; // обязательно можно Агрегация и композиция. Понятие владения Агрегация — связь между объектами типа „целое—часть“. Композиция — форма агрегации, при которой агрегат несет полную ответственность за создание и уничтожение своих частей. Если объект ответственен за удаление подобъекта, то будем говорить, что он владеет своим подобъектом. Агрегация может быть реализована следующими способами: (1) (2) (3) class A { B b; }; class A { B* b; }; class A { B& b; }; В случаях (2) и (3) не вызывается конструктор подобъекта. К моменту вызова конструктора основного объекта пододбъект уже должен быть создан. Соответственно автоматически не вызывается деструктор подобъекта. В случаях (2) и (3) реализуется меньшая „сцепка“ между классами A и B: класс A уже не обязательно ответственен за удаление объекта класса B. Т. е. объект класса A в случаях (2) и (3) может не владеть своими подобъектами. В случае (2) реализуется наименьшая „сцепка“ между классами: объект класса A может менять указатель на подобъект в ходе своего существования. Случаи (2) и (3) могут использоваться не только для моделирования агрегации, но и для моделирования более общих связей между объектами, например, отношения использования (одному объекту для правильного функционирования необходимы услуги другого объекта). Реализация владения в случаях (2) и (3): или class A { B* b; public: A(const B* bb): b(&bb) {} ~A() {delete b;} class A { B& b; public: A(const B& bb): b(bb) {} ~A() {delete &b;} }; }; Проблемы с владением (!) [ЦВВ] Для описания отношения владения удобно использовать метафору „хозяин“–„слуга“ (соответственно клиент–сервер). При этом ограничиваются следующими предположениями (из которых не все очевидны): • Слуга может принадлежать нескольким хозяевам. • Не должно быть неуправляемых слуг (утечек памяти), слуга без хозяина доложен умереть. Это правило реализуется при помощи подсчета ссылок. • Хозяин может сменить слугу (возможно став при этом совладельцем). • Иногда у совладельцев могут происходить коллизии в интересах относительно слуги. Для преодоления этой трудности иcпользуется механизм copy-on-write (см. далее). I. Запрещено владеть статическим или автоматическим объектом B b(1); A a(1); // при вызове деструк тора произойтет ошибка Способы устранения 1) Инициализация полей b: A(const B& bb) : b(*new B(bb)) {} // равносильно включению объекта : B b; ~A() {delete *b;} 2) Запрещение создания объекта в нединамической памяти: class B { B& operator=(const B&); // запрещено копировать присваиванием! [Страуструп , ДиЭС++] B(const B&); // без определения!!! Запрещаем создавать копию! [Страуструп , ДиЭС++] B() {...} // в секции private! public: static B* make() {return new B;} // или с параметрами. Это т .н. псевдоконструк тор }; Тогда A a(*B::make()); или B& b = B::make(); A a(b); II. „Слуга двух господ“ B* b = new B(1); A a1(*b), a2(*b); // ошибка при вызове деструк тора второго объекта Способ устранения — подсчет количества ссылок (хозяев). Реализация класса слуги (сервера): class B { int nRef; string name; public: B() : nRef(0) {} void link() {nRef++;} void unlink() {if (--nRef==0) delete this;} }; Реализация класса хозяина (клиента): class A { B* b; public: A(const B& bb) : b(*bb) {b->link();} ~A() {b->unlink();} void changrB(const B& bb) {b->unlink(); b = bb; b->link();} A& operator=(const A& a) { if (this != &a) changeB(a.b); return *this; } A(const A& a) { b = a.b; b->link(); } }; Ответственность по удалению B можно возложить как на A, так и на B. Вторая сторона: копирование при записи (copy on write) [Эккель, Философия С++]. Пока ни один из господ не просит слугу измениться, может использоваться одна копия слуги на двух господ. Как только один из господ пытается изменить какие-то поля слуги, создается копия слуги. Функция изменения полей слуги должна быть у господина. Поддержка механизма копирования при записи со стороны слуги: class B { ... void rename(const string& s) {name = s;} B* unalias() { if (nRef==1) return this; nRef--; return new A(*this); } }; Поддержка механизма копирования при записи со стороны хозяина: class A { ... void renameB(const string& s) { b = b->unalias(); b->rename(s); } }; Наконец, нужно запретить создавать и присваивать объекты класса B явно. (!) В концепции copy on write создавать копию при записи может только владелец. Т. е. функция записи в подобъект должна быть у владельца. Например, список объектов. При его копировании копируются указатели, а при записи в элемент (через список!) создается копия (если число ссылок > 1).