1. Подобъекты. Отношения включения между объектами

advertisement
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).
Download