|
Программирование >> Разработка устойчивых систем
Каждый из подобъектов Left и Right содержит указатель (или его концептуальный эквивалент) на общий подобъект Тор, а все обращения к этому подобъекту из функций Left и Right осуществляются через эти указатели. Теперь повыщение Bottom до Тор становится однозначным, потому что существует только один объект Тор, к которому может осуществляться преобразование. Результат выполнения этой программы выглядит так: 1.2.3.4 1245032 1 1245060 1245032 Выведенные адреса наводят на мысль, что в этой конкретной реализации подобъект Тор хранится в конце полного объекта (хотя на самом деле неважно, где именно он находится). Преобразование к void* через dynamic cast всегда дает адрес полного объекта. Хотя с технической точки зрения это незаконно, при удалении виртуального деструктора (и dynamic cast, чтобы программа компилировалась) размер Bottom уменьшается до 24 байт. Похоже, достигается экономия в размере трех указателей. Почему? Не стоит воспринимать эти числа слишком буквально. В других компиляторах добавление виртуального конструктора увеличивает размер объекта всего на четыре байта. Наверное, эти секреты могут раскрыть только сами разработчики компиляторов. А мы лишь можем сказать, что при множественном наследовании производный объект должен вести себя так, словно он содержит несколько таблиц указателей VPTR, по одному для каждого из его непосредственных базовых классов, содержащих виртуальные функции. Ни больше, ни меньше. Разработчики компиляторов могут использовать любые оптимизации по своему усмотрению, но поведение должно оставаться одним и тем же. В этой программе самое странное впечатление производит инициализатор Тор в конструкторе Bottom. Обычно нам не приходится беспокоиться об инициализации других подобъектов, кроме непосредственных базовых классов, поскольку эти Присутствие этих указателей объясняет, почему размер b заметно превышает размер четырех целых чисел. Этот факт (отчасти) объясняется затратами, связанными с использованием виртуальных базовых классов. Кроме того, необходимо учитывать затраты на хранение VPTR, обусловленные наличием виртуального деструктора. Базовые классы должны иметь виртуальные деструкторы, но многие компиляцоры позволяют проводить подобные эксперименты. классы сами обеспечивают инициализацию своих баз. Тем не менее, от Bottom к Тор ведут разные пути, поэтому надежда на передачу необходимых инициализацион-ных данных промежуточными классами Left и Right создает неоднозначность: кто именно должен отвечать за инициализацию? По этой причине виртуальная база должна инициализироваться последним производным классом в иерархии. А как насчет выражений в конструкторах Left и Right, которые тоже инициализируют Тор? Конечно, они необходимы для создания самостоятельных объектов Left и Right, но при создании объекта Bottom эти выражения должны игнорироваться (отсюда нули в их инициализаторах в конструкторе Bottom - при выполнении конструкторов Left и Right в контексте объекта Bottom любые значения в этих позициях игнорируются). Компилятор позаботится обо всем за вас, но вы должны понимать, кто и за что отвечает. Всегда следите за тем, чтобы все конкретные (не абстрактные) классы в иерархиях множественного наследования обеспечивали должную инициализацию всех своих виртуальных базовых классов. Эти правила относятся не только к инициализации, но и ко всем операциям, распространяющимся на иерархию классов. Рассмотрим оператор записи в поток из предыдущего фрагмента. Данные были объявлены защищенными, чтобы можно было сжульничать и обратиться к унаследованным данным из операторной функции operator (ostream&,const Bottom&). Обычно бывает разумнее поручить вывод каждого подобъекта соответствующему классу и предоставить производному классу вызывать функции базовых классов по мере необходимости. Но что произойдет, если мы попытаемся использовать этот подход с оператором , как показано в следующем фрагменте? : C09:VirtualBase2.cpp Пример того, как НЕ СЛЕДУЕТ реализовывать оператор linclude <iostream> using namespace std; class Top { int x; public; Top(int n) { X = n; } friend ostreamS operator (ostream& os. const Top& t) { return OS t.x; class Left : virtual public Top { int y; public: Left(int m. int n) ; Top(m) { у = n; } friend ostream& operator (ostream& os. const Left& 1) { return OS static cast<const Тор&>(1) , l.y; class Right ; virtual public Top ( int z; public; Right(int m. int n) : Top(m) { z = n; } friend ostream& operator (ostream& os. const Right& r) { return OS static cast<const Top&>(r) . r.z; class Bottom : public Left, public Right { int w; public: Bottom(int i. int j. int k. int m) : Top(i). Left(0. j). Right(0. k) { w = m; } friend ostream& operator (ostream& os. const Bottom& b) { return OS static cast<const Left&>(b) . static cast<const Right&>(b) , b.w: int mainO { Bottom b(l. 2. 3. 4): cout b endl: 1.2.1.3.4 } III:- Нельзя просто передать ответственность наверх, как обычно, потому что оператор вывода каждого из классов Left и Right вызывает оператор вывода Тор, а это ведет к дублированию данных. Взамен необходимо вручную имитировать то, что компилятор делает при инициализации. В одном из решений в классах определяются специальные функции, которые знают о сушествовании виртуального базового класса и игнорируют его при выводе (так что решение задачи остается на долю последнего производного класса): : C09:VirtualBase3.cpp Правильная реализация оператора #include <iostream> using namespace std: class Top { int x: public: Top(int n) { X = n: } friend ostream& operator (ostream& os. const Top& t) { return OS t.x: class Left : virtual public Top { int y: protected: void specialPrint(ostream& os) const { Выводятся только данные Left OS . у: public: Left(int m. int n) : Top(m) { у = n: } friend ostream& operator (ostream& os. const Left& 1) {
|
© 2006 - 2024 pmbk.ru. Генерация страницы: 0.001
При копировании материалов приветствуются ссылки. |