CHAP5 (1018805), страница 4
Текст из файла (страница 4)
{
switch( i )
{
case 1: сделать_нечто(); break;
case 2: сделать_нечто_другое(); break;
default:
fprintf(stderr, "Внутренняя ошибка в f(): неверное значение i (%d)", i );
exit( -1 );
}
}
То же самое относится к блокам if/else, работающим в манере, схожей с оператором switch.
В цикле также нужна проверка на невероятное. Следующий фрагмент работает, даже если i первоначально равно 0 - чего по идее быть не должно:
f( int i ) // переменная i должна быть положительной
{
while ( --i >= 0 )
сделать_нечто();
}
Конструкция while(--i) менее надежна, так как она дает ужасный сбой в случае, если i сначала равно 0.
66.2. Всегда проверяйте код, вызывающий ошибку.
Это должно быть очевидно, но комитет ISO/ANSI по С++ потребовал, чтобы оператор new вызывал исключение, если он не смог выделить память, потому что было установлено, что удивительное множество ошибок во время выполнения в реальных программах вызвано тем, что люди не потрудились проверить, не возвратил ли new значение NULL.
Мне также довелось видеть множество программ, в которых люди не позаботились посмотреть, сработала ли функция fopen(), перед тем как начать пользоваться указателем FILE.
67. Избегайте явно временных переменных.
Большинство переменных, используемых лишь один раз, попадают в эту категорию. Например, вместо:
int x = *p++;
f( x );
должно быть:
f( *p++ );
Редко бывает, что полезна явная временная переменная, если вам нужно гарантировать порядок вычислений, или если выражение просто такое длинное, что его невозможно прочитать. В последнем случае имя переменной даст полезную информацию и, будучи выбрано правильно, может устранить необходимость в комментарии. Например, вы можете заменить:
f( Coefficient_of_lift * (0.5 * RHO * square(v)) ); // передать функции f() образующуюся
// подъемную силу
f( Коэффициент_подъемной_силы * (0.5 * RHO * квадрат(v)) ); // передать функции f()
// образующуюся подъемную
// силу
на:
double lift = Coefficient_of_lift * (0.5 * RHO * square(v));
f( lift );
double подъемная_сила = Коэффициент_подъемной_силы * (0.5 * RHO * квадрат(v));
f( подъемная_сила );
Это правило не запрещает ни одно из подобных применений, а является, скорее, вырожденным случаем того, что упомянуто мной вначале.
68. Не нужно магических чисел.
В основном тексте вашей программы не должно быть чисел в явном виде. Используйте перечислитель или константу для того, чтобы дать числу символическое имя. (Я уже объяснял, почему для этого не очень хорошо применять #define). Тут есть два преимущества:
-
Символическое имя делает величину самодокументируемой, устраняя необходимость в комментарии.
-
Если число используется более чем в одном месте, то менять нужно лишь одно место - определение константы.
Я иногда делаю исключение из этого правила для локальных переменных. Например, в следующем фрагменте используется магическое число (128):
f()
{
char buf[128]
...
fgets( buf, sizeof (buf) / sizeof(*buf), stdin );
}
Я использовал sizeof() в вызове fgets(), поэтому изменения размера массива автоматически отражаются в программе. Добавление дополнительного идентификатора для хранения размера добавит излишнюю сложность.
69. Не делайте предположений о размерах.
Классической проблемой является код, исходящий из того, что тип int имеет размер 32 бита. Следующий фрагмент не работает, если у вас 32-битный указатель и 16-битный тип int (что может быть при архитектуре Intel 80x86):
double a[1000], *p = a;
// ...
dist_from_start_of_array_in_bytes = (int)p - (int)a;
расстояние_от_начала_массива_в_байтах = (int)p - (int)a;
Более трудно уловима такая проблема в С (но не в С++):
g()
{
не_работает( 0 );
}
не_работает( char *p )
{
if( !p ) // вероятно не работает
// ...
}
Компилятор соглашается с этим вызовом, потому что в С разрешены ссылки вперед (и не разрешены в С++, так что там это не проблема). 0 это тип int, поэтому в стек помещается 16-битовый объект. Но функция ожидает 32-битный указатель, поэтому она использует 16 бит из стека и добавляет к ним еще 16 бит всякого мусора для создания 32-битного указателя. Вероятнее всего, что if( !p ) даст ложный результат, так как только 16 бит из 32 будут равны 0.
Традиционное решение состоит в использовании typedef :
typedef int word; // всегда 16 бит
typedef long dword; // всегда 32 бита.
После чего вы можете поменять операторы typedef в новой операционной среде, чтобы гарантировать, что word по прежнему имеет размер 16 бит, а dword - 32 бита. Для 32-разрядной системы предыдущее может быть переопределено как:
typedef short word; // всегда 16 бит
typedef int dword; // всегда 32 бита.
Другая связанная с размерностью часовая бомба спрятана в том способе, которым в ANSI С обеспечивается работа с иностранными языками. ANSI С определяет тип wchar_t для работы с расширенными наборами символов типа Unicode - нового 16-битного многонационального набора символов. Стандарт ANSI С также утверждает, что перед строкой с расширенными символами должен стоять символ L. Micrisoft и другие поставщики компиляторов стараются помочь вам писать переносимые программы, предусматривая макросы типа:
#ifdef _UNICODE
typedef wchar_t _TCHAR
# define _T(x) L##x
#else
typedef char _TCHAR
# define _T(x) x
#endif
Если константа _UNICODE не определена, то оператор:
_TCHAR *p = _T("делай_что_нужно");
имеет значение:
char *p = "делай_что_нужно";
Если константа _UNICODE определена, тот же самый оператор получает значение:
wchar_t *p = L"делай_что_нужно";
Пока все хорошо. Вы теперь можете попробовать перенести вашу старую программу в среду Unicode, просто используя свой редактор для замены всех экземпляров char на _TCHAR и помещения всех строковых констант в скобки _T(). Проблема состоит в том, что такой код, как ниже (в котором все _TCHAR первоначально были типа char), более не работает:
_TCHAR str[4];
// ...
int max_chars = sizeof(str); // предполагает, что тип char имеет размер 1 байт
Тип _TCHAR будет иметь размер 2 байта при определенной константе _UNICODE, поэтому число символов у вас будет определено в два раза большим, чем есть на самом деле. Для исправления ситуации вы должны воспользоваться следующим вариантом:
int max_chars = sizeof(str) / sizeof(*str) ;
70. Опасайтесь приведения типов (спорные вопросы С).
Оператор приведения типов часто понимается неправильно. Приведение типов не указывает компилятору "считать эту переменную принадлежащей к этому типу". Оно должно рассматриваться как операция времени выполнения, которая создает временную переменную типа, определенного для приведения, затем инициализирует эту временную переменную от операнда. В С++, конечно, эта инициализация может обернуться очень большими накладными расходами, так как возможен вызов конструктора.
Первое место, где неверное понимание приведения типов может навлечь на вас неприятности, находится в С, где не требуются прототипы функций. Когда компилятор находит вызов функции без предшествующего прототипа, то полагает, что эта функция возвращает тип int. В следующем фрагменте не говориться "malloc() на самом деле возвращает указатель, а не тип int":
int *p = (int *) malloc( sizeof(int) );
а скорее код говорит "я полагаю, что malloc() возвращает тип int, так как тут нет предшествующего прототипа, и преобразую этот int в указатель для присваивания его значения p). Если тип int имеет размер 16 бит, а указатель 32-битовый, то вы теперь в глубокой луже. Вызов malloc() может вернуть и 32-битовый указатель, но так как компилятор полагает, что malloc() возвращает 16-битовый int, то он игнорирует остальные 16 бит. Затем компилятор округляет возвращенное значение до 16-бит и преобразует его в 32-битовый тип int принятым у него способом, обычно заполняя старшие 16 бит нулями. Если указатель содержал адрес больше, чем 0xffff, что вероятно для большинства компьютеров, то вы просто теряете старшие биты. Единственным способом урегулирования этой проблемы является указание для malloc() соответствующего прототипа, который подскажет, что malloc() возвращает указатель (обычно путем включения файла <stdlib.h>).
Следующий проблема состоит в том, что предыдущее приведение типа может быть вставлено в программу просто для того, чтобы заставить заткнуться компилятор, который наверняка будет выдавать предупреждение о несоответствии типов, если оператор приведения отсутствует. Приведением типов часто злоупотребляют подобным образом - чтобы заглушить компилятор, вместо того чтобы в самом деле обратить внимание на предупреждение. Многие компиляторы, например, выдают предупреждение о возможном округлении, встретив следующий код:
f( int x );
// ...
unsigned y;
f( y );
и многие программисты заглушат такой компилятор при помощи f((int)y). Несмотря на это, приведение типа не изменит того факта, что тип unsigned int может содержать такое значение, которое не поместится в int со знаком, поэтому результирующий вызов может не сработать.
Вот сходная проблема, связанная с указателями на функции. Следующий код, случается, работает отлично:
some_object array[ size ];
int my_cmp( some_object *p1, some_object *p2 );
qsort( array, size, sizeof(some_object), ((*)(void*, void*)) my_cmp );
Следующий похожий код просто печально отказывается работать без предупреждающего сообщения:
some_object array[ size ];
void foo( int x );
qsort( array, size, sizeof(some_object), ((*)(void*, void*)) foo );
Функция qsort() передает аргументы-указатели в foo(), но foo() ждет в качестве аргумента int, поэтому будет использовать значение указателя в качестве int. Дальше еще хуже - foo() вернет мусор, который будет использован qsort(), так как она ожидает в качестве возвращаемого значения int.
Выравнивание также связано с затруднениями. Многие компьютеры требуют, чтобы объекты определенных типов располагались по особым адресам. Например, несмотря на то, что 1-байтоый тип char может располагаться в памяти по любому адресу, 2-байтовый short должен будет иметь четный адрес, а 4-байтовый long - четный и кратный четырем. Следующий код вновь не выдаст предупреждений, но может вызвать зависание компьютера во время выполнения:
short x;
long *lp = (long*)( &x );
*lp = 0;
Эта ошибка особенно опасна, потому что *lp = 0 не сработает лишь тогда, когда x окажется по нечетному или не кратному четырем адресу. Может оказаться, что этот код будет работать до тех пор, пока вы не добавите объявление второй переменной типа short сразу перед x, после чего эта программа зависнет.
Один из известных мне компиляторов пытается справиться с этой проблемой фактически модифицируя содержимое указателя для того, чтобы гарантировать правильный адрес в качестве побочного эффекта от приведения типа. Другими словами, следующий код мог бы на самом деле модифицировать p:
p = (char *)(long *);
71. Немедленно обрабатывайте особые случаи.
Пишите свои алгоритмы таким образом, чтобы они обрабатывали вырожденные случаи немедленно. Вот тривиальный пример того, как сделать это неправильно:
print( const char *str )
{
if ( !*str ) // ничего не делать, строка пуста
return;
while ( *str )
putchar( *str++ );
}
Оператор if тут не нужен, потому что этот случай хорошо обрабатывается циклом while.
Листинги 2 и 3 демонстрируют более реалистический сценарий. Листинг 2 определяет умышленно наивный заголовок связанного списка и функцию для удаления из него элемента.
Листинг 2. Связанный список: Вариант 1.
-
typedef struct node
-
{
-
struct node *next, *prev;
-
//...
-
} node;
-
node *head;
-
remove( node **headp, node *remove )
-
{
-
// Удалить элемент, на который указывает "remove", из списка, на начало
-
// которого указывает *headp.
-
if ( *headp == remove ) // Этот элемент в начале списка.
-
{
-
if ( remove->next ) // Если это не единственный элемент в списке,
-
remove->next->prev = NULL; // то поместите следующий за ним элемент
-
// первым в списке.
-
*headp = remove->next;
-
}
-
else // Элемент находится в середине списка
-
{
-
remove->prev->next = remove->next;
-
if ( remove->next )
-
remove->next->prev = remove->prev;
-
}
-
}
Листинг 3 делает то же самое, но я модифицировал указатель на предыдущий элемент в структуре node, поместив туда адрес поля next предыдущего элемента вместо указателя на всю структуру. Это простое изменение означает, что первый элемент больше не является особым случаем, поэтому функция remove становится заметно проще.
Смысл этого состоит в том, что незначительная переделка этой задачи позволяет мне использовать алгоритм, не имеющий особых случаев, этим самым упрощая программу. Конечно, эта простота не дается бесплатно - теперь стало невозможным перемещение по списку в обратном направлении - но нам ведь это может быть и не нужно.
Листинг 3. Связанный список: Вариант 2.
-
typedef struct node
-
{
-
struct node *next, **prev; // <=== К prev добавлен символ *
-
// ...
-
} node;
-
node *head;
-
remove( node **headp, node *remove )
-
{
-
if ( *(remove->prev) = remove->next ) // если не в конце списка,
-
remove->next->prev = remove->prev; // то уточнить следующий элемент
-
}
72. Не старайтесь порадовать lint.
Lint является программой проверки синтаксиса для языка С. (Также имеется версия для С++ в среде MS-DOS/Windows. Она выпускается фирмой Gimple Software). Хотя эти программы неоценимы при использовании время от времени, они выводят такую кучу сообщений об ошибках и предупреждений, что текст вашей программы будет почти невозможно прочитать, если вы попробуете избавиться от всех них. Оказывается, нужно избегать кода, подобного следующему:















