|
Программирование >> Арифметические и логические операции
использовать. При всем при том, как показывает практика, ошибка, связанная с отсутствием виртуальных деструкторов, повсеместно распространена. Итак, рассмотрим небольшой пример: class A public: virtual void f() = 0; ~A(); class B : public A public: virtual void f(); ~B(); Вызов компилятора gcc строкой: g++ -c -Wall test.cpp даст следующий результат: test.cpp:6: warning: class A has virtual functions but non-virtual destructor test.cpp:13: warning: class B has virtual functions but nonvirtual destructor Это всего лишь предупреждения, компиляция прошла вполне успешно. Однако, почему же gcc выдает подобные предупреждения? Все дело в том, что виртуальные функции используются в C++ для обеспечения полиморфизма - т.е., клиентская функция вида: void call f(A* a) a->f(); никогда не знает о том, что конкретно сделает вызов метода f() - это зависит от того, какой в действительности объект представлен указателем a. Точно так же сохраняются указатели на объекты: std::vector<A*> a collection; a collection.push back(new B()); В результате такого кода теряется информация о том, чем конкретно является каждый из элементов a collection (имеется в виду, без использования RTTI). В данном случае это грозит тем, что при удалении объектов: for(std::vector<A*>::iterator i = ... ) delete *i; все объекты, содержащиеся в a collection, будут удалены так, как будто это - объекты класса A. В этом можно убедиться, если соответствующим образом определить деструкторы классов A и B: inline A::~A() puts( A::~A() ); inline B::~B() puts( B::~B() ); Тогда выполнение следующего кода: A* ptr = new B(); delete ptr; приведет к следующему результату: A::~A() Если же в определении класса A деструктор был бы сделан виртуальным (virtual ~A();), то результат был бы другим: B::~B() A::~A() В принципе, все сказано. Но, несмотря на это, очень многие программисты все равно не создают виртуальных деструкторов. Одно из распространенных заблуждений - виртуальный деструктор необходим только в том случае, когда на деструктор порожденных классов возлагаются какие-то нестандартные функции; если же функционально деструктор порожденного класса ничем не отличается от деструктора предка, то делать его виртуальным совершенно необязательно. Это неправда, потому что даже если деструктор никаких специальных действий не выполняет, он все равно должен быть виртуальным, иначе не будут вызваны деструкторы для объектов-членов класса, которые появились по отношению к предку. То есть: #include <stdio.h> class A public: A(const char* n); ~A(); protected: const char* name; inline A::A(const char* n) : name(n) inline A::~A() printf( A::~A() for °/os.\n , name); class B public: virtual void f(); B(); ~B(); protected: A a1; inline B::~B() inline B::B() : a1( a1 ) void B::f() { } class C : public B public: C(); protected: A a2; inline C::C() : a2( a2 ) int main() B* ptr = new C(); delete ptr; return 0; Компиляция данного примера проходит без ошибок (но с предупреждениями), вывод программы следующий: A::~A() for a1 Немного не то, что ожидалось? Тогда поставим перед названием деструктора класса B слово virtual. Результат изменится: A::~A() for a2 A::~A() for a1 Сейчас вывод программы несколько более соответствует действительности. Глава 8. Запись структур данных в двоичные файлы Чтение и запись данных, вообще говоря, одна из самых часто встречающихся операций. Сложно себе представить программу, которая бы абсолютно не нуждалась бы в том, чтобы отобразить где-нибудь информацию, сохранить промежуточные данные или, наоборот, восстановить состояние прошлой сессии работы с программой. Собственно, все эти операции достаточно просто выполняются - в стандартной библиотеке любого языка программирования обязательно найдутся средства для обеспечения ввода и вывода, работы с внешними файлами. Но и тут находятся некоторые сложности, о которых, обычно, не задумываются. Итак, как все это выглядит обычно? Имеется некоторая структура данных: struct data item type 1 field 1; type 2 field 2; ... type n field n; data item i1; Каким образом, например, сохранить информацию из i1 так, чтобы программа во время своего повторного запуска, смогла восстановить ее? Наиболее частое решение следующее: FILE* f = fopen( file , wb ); fwrite((char*)&i1, sizeof(i1), 1, f); fclose(f); assert расставляется по вкусу, проверка инвариантов в данном примере не является сутью. Тем не менее, несмотря на частоту использования, этот вариант решения проблемы не верен. Нет, он будет компилироваться и, даже будет работать. Мало того, будет работать и соответствующий код для чтения структуры: FILE* f = fopen( file , rb ); fread((char*)&i1, sizeof(i1), 1, f); fclose(f); Что же тут неправильного? Ну что же, для этого придется немного пофилософствовать. Как бы много не говорили о том, что С - это почти то же самое, что и ассемблер, не надо забывать, что он является все-таки языком высокого уровня. Следовательно, в принципе, программа написанная на С (или C++) может (теоретически) компилироваться на разных компиляторах и разных платформах. К чему это? К тому, что данные, которые сохранены подобным образом, в принципе не переносимы. Стоит вспомнить о том, что для структур неизвестно их физическое представление. То есть, для конкретного компилятора оно, быть может, и известно (для этого достаточно посмотреть работу программы вооруженным взглядом , т.е. отладчиком), но о том, как будут расположены в памяти поля структуры на какой-нибудь оригинальной машине, неизвестно. Компилятор со спокойной душой может перетасовать поля (это, в принципе, возможно) или выровнять положение полей по размеру машинного слова (встречается сплошь и рядом). Для чего? Для увеличения скорости доступа к полям. Понятно, что если поле начинается с адреса, не кратного машинному слову, то прочитать его содержимое не так быстро, как в ином случае. Таким образом, сохранив данные из памяти в бинарный файл напрямую мы получаем дамп памяти конкретной архитектуры (не говоря о том, что sizeof совершенно не обязан возвращать количество байт). Плохо это тем, что при переносе данных на другую машину при попытке прочитать их той же программой (или программой, использующую те же структуры) вполне можно ожидать несколько некорректных результатов. Это связано с тем, что структуры могут быть представлены по другому в памяти (другое выравнивание), различается порядок следования байтов в слове и т.п. Как этого избежать? Обычный костыль , который применяется, например, при проблемах с выравниванием, заключается в том, что компилятору явно указывается как надо расставлять поля в структурах. В принципе, любой компилятор дает возможность управлять выравниванием. Но выставить одно значение для всего проекта при помощи ключей компилятора (обычно это значение равно 1, потому что при этом в сохраненном файле не будет пустых мест) нехорошо, потому что это может снизить скорость выполнения программы. Есть еще один способ указания компилятору размера выравнивания, он заключается в использовании директивы препроцессора #pragma. Это не оговорено стандартом, но обычно есть директива #pragma pack, позволяющая сменить выравнивание для определенного отрезка исходного текста. Выглядит это обычно примерно так: #pragma pack(1) struct { /* #pragma pack(4) Последняя директива #pragma pack(4) служит для того, чтобы вернуться к более раннему значению выравнивания. В принципе, конечно же при написании исходного текста никогда доподлинно заранее неизвестно, какое же было значение выравнивания до его смены, поэтому в некоторых компиляторах под Win32 есть возможность использования стека значений (пошло это из MS Visual C++): #pragma pack(push, 1) struct { /* ... */ }; #pragma pack(pop) В примере выше сначала сохраняется текущее значение выравнивания, затем оно заменяется 1, затем восстанавливается ранее сохраненное значение. При этом, подобный синтаксис поддерживает даже gcc для win32 (еще стоит заметить, что, вроде, он же под Unix использовать такую запись #pragma pack не дает). Есть альтернативная форма #pragma pack(), поддерживаемая многими компиляторами (включая msvc и gcc), которая устанавливает значение выравнивания по умолчанию. И, тем не менее, это не хорошо. Опять же, это дает очень интересные ошибки. Представим себе следующую организацию исходного текста. Сначала заголовочный файл inc.h: #ifndef inc h #define inc h class Object
|
© 2006 - 2025 pmbk.ru. Генерация страницы: 0.001
При копировании материалов приветствуются ссылки. |