|
Программирование >> Арифметические и логические операции
}; #endif inc h Представьте себе, что существуют три файла filel.cpp, file2.cpp и file2.h, которые этот хидер используют. Допустим, что в file2.h находится функция foo, которая (например) записывает Object в файл: file1.cpp #include inc.h #include file2.h int main() Object* ob] = new Ob]ect(); foo(ob], file ); delete obj; return 0; file2.h #ifndef file2 h #define file2 h #pragma pack(1) #include inc.h void foo(const Object* obj, const char* fname); #pragma pack(4) #endif file2 h file2.cpp #include file2.h void foo(const Object* obj, const char* fname) ... Это все скомпилируется, но работать не будет. Почему? Потому что в двух разных единицах компиляции (filel.cpp и file2.cpp) используется разное выравнивание для одних и тех же структур данных (в данном случае, для объектов класса Object). Это даст то, что объект переданный по указателю в функцию foo() из функции main() будет разным (и, конечно же, совсем неправдоподобным). Понятно, что это явный пример плохой организации исходных текстов - использование директив компилятора при включении заголовочных файлов, но, поверьте, он существует. Отладка программы, содержащую подобную ошибку, оказывается проверкой на устойчивость психики. Потому что выглядит это примерно так: следим за объектом, за его полями, все выглядит просто замечательно и вдруг, после того как управление передается какой-то функции, все, что содержится в объекте, принимает бредовые формы, какие-то неизвестно откуда взявшиеся цифры... На самом деле #pragma pack не является панацеей. Мало того, использование этой директивы практически всегда неправомерно. Можно даже сказать, что эта директива в принципе редко когда нужна (во всяком случае, при прикладном программировании). Правильным же подходом является сначала записать все поля структуры в нужном порядке в некоторый буфер и скидывать в файл уже содержимое буфера. Это очень просто и очень эффективно, потому что все операции чтения/записи можно собрать в подпрограммы и менять их при необходимости таким образом, чтобы обеспечить нормальную работу с внешними файлами. Проиллюстрируем этот подход: template<class T> inline size t get size(const T& obj) return sizeof(ob]); Эта функция возвращает размер, необходимый для записи объекта. Зачем она понадобилась? Во-первых, возможен вариант, что sizeof возвращает размер не в байтах, а в каких-то собственных единицах. Во-вторых, и это значительно более необходимо, объекты, для которых вычисляется размер, могут быть не настолько простыми, как int. Например: template<> inline size t get size<std::string>(const std::string& s) return s.length() + 1; Надеемся, понятно, почему выше нельзя было использовать sizeof. Аналогичным образом определяются функции, сохраняющие в буфер данные и извлекающие из буфера информацию: typedef unsigned char byte t; template<class T> inline size t save(const T& i, byte t* buf) *((T*)buf) = i; return get size(i); template<class T> inline size t restore(T& i, const byte t* buf) i = *((T*)buf); return get size(i); Понятно, что это работает только для простых типов (int или float), уж очень много чего наворочено: явное приведение указателя к другому типу, оператор присваивания... конечно же, очень нехорошо, что такой save() доступен для всех объектов. Понятно, что очень просто от него избавиться, убрав шаблонность функции и реализовав аналогичный save() для каждого из простых типов данных. Тем не менее, это всего-лишь примеры использования: template<> inline size t save<MyObject>(const MyObject& s, byte t* buf) Можно сделать и по другому. Например, ввести методы save() и restore() в каждый из сохраняемых классов, но это не столь важно для принципа этой схемы. Поверьте, это достаточно просто использовать, надо только попробовать. Мало того, здесь можно вставить в save<long>() вызов htonl() и в restore<long>() вызов ntohl(), после чего сразу же упрощается перенос двоичных файлов на платформы с другим порядком байтов в слове... в общем, преимуществ - море. Перечислять все из них не стоит, но как после этого лучше выглядит исходный текст, а как приятно вносить изменения Глава 9. Оператор безусловного перехода goto Так уж сложилось, что именно присутствие или отсутствие этого оператора в языке программирования всегда вызывает жаркие дебаты среди сторонников хорошего стиля программирования. При этом, и те, кто за , и те, кто против всегда считают признаком хорошего тона именно использование goto или, наоборот, его неиспользование. Не вставая на сторону ни одной из этих школ , просто покажем, что действительно есть места, где использование goto выглядит вполне логично. Но сначала о грустном. Обычно в вину goto ставится то, что его присутствие в языке программирования позволяет делать примерно такие вещи: int i, j; for(i = 0; i < 10; if(condition1) j = 4; goto label1; ... for(j = 0; j < 10; j++) ... label1: ... if(condition2) i = 6; goto label2; ... label2: ... Прямо скажем, что такое использование goto несколько раздражает, потому что понять при этом, как работает программа при ее чтении будет очень сложно. А для человека, который не является ее автором, так и вообще невозможно. Понятно, что вполне вероятны случаи, когда такого подхода требует какая-нибудь очень серьезная оптимизация работы программы, но делать что-то подобное программист в здравом уме не должен. На самом деле, раз уж мы привели подобный пример, в нем есть еще один замечательный нюанс - изменение значения переменной цикла внутри цикла. Смеем вас заверить, что такое поведение вполне допустимо внутри do или while; но когда используется for - такого надо избегать, потому что отличительная черта for как раз и есть жестко определенное местоположение инициализации, проверки условия и инкремента (т.е., изменения переменной цикла). Поэтому читатель исходного текста, увидев полный for (т.е. такой, в котором заполнены все эти три места) может и не заметить изменения переменной где-то внутри цикла. Хотя для циклов с небольшим телом это, наверное, все-таки допустимо - такая практика обычно применяется при обработке строк (когда надо, например, считать какой-то символ, который идет за спецсимволом , как \\ в строках на Си; вместо того, чтобы вводить дополнительный флаг, значительно проще, увидев \ , сразу же сдвинуться на одну позицию и посмотреть, что находится там). В общем, всегда надо руководствоваться здравым смыслом и читабельностью программы. Если здравый смысл по каким-то причинам становится в противовес читабельности программы, то это место надо обнести красными флагами, чтобы читатель сразу видел подстерегающие его опасности. Тем не менее, вернемся к goto. Несмотря на то, что такое расположение операторов безусловного перехода несколько нелогично (все-таки, вход внутрь тела цикла это, конечно же, неправильно) - это встречается. Итак, противники использования goto в конечном итоге приходят к подобным примерам и говорят о том, что раз такое его использование возможно, то лучше чтобы его совсем не было. При этом, конечно же, никто обычно не спорит против применения, например, break, потому что его действие жестко ограничено. Хочется сказать, что подобную ситуацию тоже можно довести до абсурда, потому что имеются программы, в которых введен цикл только для того, чтобы внутри его тела использовать break для выхода из него (т.е., цикл делал только одну итерацию, просто в зависимости от исходного состояния заканчивался в разных местах). И что помешало автору использовать goto (раз уж хотелось), кроме догматических соображений, не понятно. Собственно, мы как раз подошли к тому, что обычно называется разумным применением этого оператора. Вот пример: switch(key1) case q1 : switch(key2) case q2 : break; break; Все упрощено до предела, но, в принципе, намек понятен. Есть ситуации, когда нужно что-то в духе break, но на несколько окружающих циклов или операторов switch, а break завершает только один. Понятно, что в этом примере читабельность, наверное, не нарушена (в смысле, использовался бы вместо внутреннего break goto или нет), единственное, что в таком случае будет выполнено два оператора перехода вместо одного (break это, все-таки, разновидность goto). Значительно более показателен другой пример: bool end needed = false; for( for( if(cond1) { end needed = true; break; } if(end needed) break; Т.е., вместо того, чтобы использовать goto и выйти из обоих циклов сразу, пришлось завести еще одну переменную и еще одну проверку условия. Тут хочется сказать, что goto в такой ситуации выглядит много лучше - сразу видно, что происходит; а то в этом случае придется пройти по всем условиями и посмотреть, куда они выведут. Надо сказать (раз уж мы начали приводить примеры из жизни), что не раз можно видеть эту ситуацию, доведенную до крайности - четыре вложенных цикла (ну что поделать) и позарез надо инициировать выход из самого внутреннего. И что? Три лишних проверки... Кроме того, введение еще одной переменной, конечно же, дает возможность еще раз где-нибудь допустить ошибку, например, в ее инициализации. Опять же, читателю исходного текста придется постоянно лазить по тексту и смотреть, зачем была нужна эта переменная... в общем: не плодите сущностей без надобности. Это только запутает.
|
© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки. |