Жанр: Учеба

Правила программирования на Си и Си плюс плюс.

сторінка №8

функции.
Следующий код, случается, работает отлично:

some_object array[ size ];
int my_cmp( some_object *p1, some_object *p2 );

qsort( array, size, sizeof(some_object),(
(*)(void*, void*)) my_cmp );


Правила обычного программирования 95

Следующий похожий код просто печально отказывается работать без
предупреждающего сообщения:

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 )
{


Правила программирования на Си и Си++

96


if
( !*str ) // ничего не делать, строка пуста
return
;
while
( *str )
putchar( *str++ );
}

Оператор if тут не нужен, потому что этот случай хорошо
обрабатывается циклом while.
Листинги 2 и 3 демонстрируют более реалистический сценарий.
Листинг 2 определяет умышленно наивный заголовок связанного списка и
функцию для удаления из него элемента.


Листинг 2. Связанный список: вариант 1

1 typedef struct node
2 {
3 struct node *next, *prev;
4 //...
5 } node;

6


7 node *head;

8


9 remove( node **headp, node *remove )
10 {
11 // Удалить элемент, на который указывает remove, из
12 // списка, на начало которого указывает *headp.

13


14 if
( *headp == remove ) // Этот элемент в начале списка.
15 {
16 if
( remove-"next ) // Если это не единственный
17 remove-"next-"prev = NULL; // элемент в списке, то
18 // поместите следующий
19 // за ним элемент первым
20 // в списке.
21 *headp = remove-"next;
22 }
23 else // Элемент находится в середине списка
24 {
25 remove-"prev-"next = remove-"next;
26 if
( remove-"next )
27 remove-"next-"prev = remove-"prev;
28 }
29 }

Листинг 3 делает то же самое, но я модифицировал указатель на
предыдущий элемент в структуре node, поместив туда адрес поля next
предыдущего элемента вместо указателя на всю структуру. Это простое
изменение означает, что первый элемент больше не является особым
случаем, поэтому функция remove становится заметно проще.
Смысл этого состоит в том, что незначительная переделка этой задачи
позволяет мне использовать алгоритм, не имеющий особых случаев, этим


Правила обычного программирования 97

самым упрощая программу. Конечно, эта простота не дается бесплатно -
теперь стало невозможным перемещение по списку в обратном
направлении - но нам ведь это может быть и не нужно.

Листинг 3. Связанный список: вариант 2

1 typedef struct node
2 {
3 struct node *next, **prev; // "== К prev добавлен символ *
4 //
5 } node;

6


7 node *head;

8


9 remove( node **headp, node *remove )
10 {
11 if
( *(remove-"prev) = remove-"next ) // если не в конце
12 remove-"next-"prev = remove-"prev; // списка, то уточнить
13 // следующий элемент
14 }

72. Не старайтесь порадовать lint
Lint является программой проверки синтаксиса для языка Си. (Также
имеется версия для Си++ в среде MS-DOS/Windows. Она выпускается
фирмой Gimple Software). Хотя эти программы неоценимы при
использовании время от времени, они выводят такую кучу сообщений об
ошибках и предупреждений, что текст вашей программы будет почти
невозможно прочитать, если вы попробуете избавиться от всех них.

Оказывается, нужно избегать кода, подобного следующему:

(void ) printf("...");

Тут вообще нет ничего неверного в присваивании в цикле:

while
( p = f() )
g(p);

даже если новичок в программировании может воспользоваться символом
=
вместо ==. (Проблемы новичков не должны приниматься во внимание,
когда вы обсуждаете рекомендуемый стиль для опытных профессионалов.
Это похоже на принятие закона, который требует, чтобы все велосипеды
оснащались боковыми колесиками, потому что двухлетним малышам без
них тяжело кататься).
Я читал предложение поручать компилятору поиск небрежных
присваиваний (когда вы на самом деле подразумевали сравнение) просто
размещая константы в левой части. Например, следующий фрагмент даст
ошибку при компиляции, если вы используете = вместо ==:


Правила программирования на Си и Си++

98


#define MAX 100
// ...
if ( MAX == x )
// ...

В этой идее есть достоинства, но я нахожу, что такой код труднее
читается.

73. Помещайте код, динамически распределяющий и
освобождающий память, в одном и том же месте
Утечки памяти - выделенную память забыли освободить - являются
большой проблемой в приложениях, продолжительность работы которых
не ограничена: серверах баз данных, торговых автоматах, операционных
системах и так далее. Имеется множество способов для того, чтобы
отслеживать эту проблему. Многие программы, например, модифицируют
функцию malloc() для создания списка выделенных областей памяти,
который может просматриваться функцией free() для проверки
задействованных указателей. Вы также можете доработать заголовочный
файл, необходимый для сопровождения списка выделенных областей
памяти, путем помещения туда информации о том, откуда было
произведено выделение памяти. (Передайте __LINE__ и _FILE__ в свою
отладочную версию malloc()). Список выделенных областей памяти
должен быть пуст при завершении работы программы. Если он не пуст, то
вы можете его просмотреть и, по крайней мере, разобраться, где была
выделена эта память.
Отслеживание - это хорошо, но предупреждение лучше. Если есть
возможность, то вы должны размещать free() в той же функции, где
сделан соответствующий вызов malloc(). Например, используйте это:

void user( void ) // пользователь
{
p = malloc( size );
producer( p ); // изготовитель
consumer( p ); // потребитель
free( p );
}

вместо этого:

void *producer( )
{
void *p = malloc( size );
// ...
return p;
}
void consumer( void *p )
{


Правила обычного программирования 99

// ...
free( p );
}
void user( void )
{
void *p = producer();
consumer( p );
}

Несмотря на это, вы не сможете всегда делать подобное на Си. Например,
предыдущие правила о неинициализированных переменных относятся и к
памяти, выделенной malloc(). Лучше сделать так:
some_object *p = allocate_and_init(); // Не возвращает значения,
// если памяти недостаточно.

чем так:

some_object *p = malloc( sizeof(some_object) );
if
( !p )
fatal_error("
Недостаточно памяти!")
init();

В Си++ эта проблема решается при помощи конструкторов.

74. Динамическая память - дорогое удовольствие
Следующей основной проблемой при использовании malloc()/free()
(или new/delete) является время, требуемое для управления памятью;
оно может быть значительным. Я однажды сократил время выполнения на
50% путем замены многочисленных вызовов malloc() и free() на
другую стратегию. Например, если у вас есть очень активно используемая
структура из одинаковых объектов, то вы можете использовать нечто
подобное коду из листинга 4 для управления членами структуры данных.

Листинг 4. Управление собственным списком высвобожденных элементов

1 typedef struct some_class
2 {
3 struct some_class *next;
4 //...
5 }
6 some_class;

7


8 static some_class *free_list = NULL;
9 //--------------------------------------------------------
10 free_object( some_class *object )
11 {
12 // Вместо того, чтобы передать память из-под объекта
13 // функции free(), свяжите ее с началом списка
14 // высвобожденных элементов.
15 object-"next = free_list;


Правила программирования на Си и Си++

100


16 free_list = object;
17 }
18 //--------------------------------------------------------
19 free_all_objects( )
20 {
21 // Высвободить все объекты в список высвобожденных
22 // элементов. Сортировка этих объектов по базовому адресу
23 // перед началом цикла улучшит скорость работы по
24 // освобождению, но я не делаю здесь этого. (Для
25 // сортировки связанных списков превосходно подходит
26 // алгоритм Quicksort).

27


28 some_object *current;
29 while
( free_list )
30 {
31 current = free_list;
32 free_list = current-"next;
33 free( current );
34 }
35 }
36 //--------------------------------------------------------
37 some_class *new_object( )
38 {
39 // Если в списке высвобожденных элементов имеется объект,
40 // то используйте его. Размещайте объект посредством
41 // malloc(),
только если список высвобожденных объектов
42 // пуст.

43 some_class *object;
44 if
( free_list )
45 {
46 object = free_list; // передайте память для объекта
47 free_list = object-"next;
48 }
49 else
50 {
51 // Вы можете улучшить производительность еще более,если
52 // будете размещать объекты в памяти по 100, а не по 1
53 // за раз, но это усложнит функцию free_all_objects().

54


55 object = malloc( sizeof(some_class) );
56 }

57


58 if
( object )
59 // поместите здесь инициализирующий код

60


61 return object;
62 }

75. Тестовые подпрограммы не должны быть
интерактивными
Мне часто показывают тестовые программы, имеющие


Правила обычного программирования 101

усовершенствованный интерактивный интерфейс пользователя. Это не
только является пустой тратой времени, но и не может быть использовано
для тщательного тестирования. Люди, сидящие за клавиатурами и
пробующие все, что им приходит в голову, недостаточно методичны.
Поэтому для систематического выполнения функций, подлежащих
проверке, лучше использовать неинтерактивную тестовую функцию.
И, кстати, не удаляйте эту тестовую процедуру; просто используйте
предложение #ifdef TEST для включения или выключения этой
подпрограммы из компиляции. Если вы похожи на меня, то вы утром
удалите эту тестовую функцию, а уже к обеду она вам понадобиться
снова, даже если вы не пользовались ей последние два года.

76. Сообщение об ошибке должно подсказывать
пользователю, как ее исправить
Когда-то, во времена CP/M, отладчик DDT имел единственное сообщение
об ошибке. Не имело значения, что вы натворили, он всегда говорил:

?

Несмотря на то, что подобная обработка ошибок, к счастью, больше не
является нормой, я все еще вижу ее, хотя и с более прихотливыми
украшениями. Несколько программ для Windows, мне принадлежащие,
просто издают звуковой сигнал, когда я ввожу неверное значение в
диалоговом окне, заставляя меня гадать, какое же значение верное. Я
часто видел сообщения об ошибках, которые давали мне обильное
количество информации о том, что я сделал неправильно, не давая
подсказки, как это делается правильно. В самой печальной ситуации
диалоговое меню предлагает вам ввести число, но когда вы вводите
неверное число, оно выводит такое сообщение:
Неверное значение.

без намека на то, какое значение будет верным. Иногда диалоговое окно
отказывается закрыться до тех пор, пока вы не введете верное значение.
Иногда программа захватывает управление компьютером, и вы не можете
переключиться в другое приложение, завершив то, в котором с вами
случилась беда. (В Windows это "свойство" называется системномодальным
диалоговым окном. Пожалуйста, не пользуйтесь им). И вот вы
перед выбором: завершить программу, нажав Ctrl-C или ее эквивалент
(если можете), отключить питание или наобум набирать числа на
клавиатуре до тех пор, пока вам не удастся наткнуться на то, которое
удовлетворит программу ввода данных. Вопрос состоит в том, что займет
у вас больше времени: повторить последние три часа работы, которую вы


Правила программирования на Си и Си++

Посмотри в окно!

Чтобы сохранить великий дар природы — зрение, врачи рекомендуют читать непрерывно не более 45–50 минут, а потом делать перерыв для ослабления мышц глаза. В перерывах между чтением полезны гимнастические упражнения: переключение зрения с ближней точки на более дальнюю.

102


не подумали сохранить перед вызовом диалогового окна, или потратить
еще три часа, играя в "холодно-горячо" с диалоговым окном. Это как раз
та ситуация, из-за которой у компьютеров дурная слава.
Сообщение об ошибке должно подсказывать вам, как исправить эту
ситуацию, что-нибудь типа:
Числа должны быть в диапазоне от 17 до 63 включительно.

или:
Даты должны иметь формат дд-мм-гггг.

Должен быть какой-то способ (типа клавиши "help/cправка") для
получения дополнительной информации, если она вам нужна. Наконец, у
вас должен быть способ безопасного прекращения процесса ввода данных
(типа клавиши "Выход").

77. Не выводите сообщения об ошибке, если она
исправима
Библиотечные подпрограммы (и большинство подпрограмм на языках
низкого уровня) не должны выводить сообщений об ошибках. Они
должны возвращать код ошибки, который может быть проверен
вызывающей подпрограммой. Таким способом эта подпрограмма может
предпринять какие-то корректирующие действия. Например, если
рекурсивная функция-сортировщик сообщила об ошибке, выйдя за
пределы стека, то вызвавшая ее подпрограмма не сможет попытаться
заново выполнить сортировку с использованием другого, нерекурсивного
алгоритма. Пользователь должен увидеть сообщение об ошибке, даже
если вторая попытка сортировки будет удачной.

78. Не используйте системно-зависимых функций
для сообщений об ошибках
Многие среды с оконным интерфейсом не поддерживают понятия
стандартного устройства для вывода или для сообщений об ошибках. (В
этих средах вызовы printf() или fprintf(stderr, ...) обычно
игнорируются). Если вы будете основываться на догадках о своей среде,
то обнаружите, что вам необходимы значительные доработки просто для
того, чтобы перекомпилировать код для новой среды).
Минимум вашей защиты может быть таким:

#define error printf
#define terminate(x) ExitProcess(x)

после чего используйте:


Правила обычного программирования 103

if ( some_error )
{
error("
Тут что-то не так ");
terminate( -1 );
}

Вот более гибкое решение:

#include "stdio.h"
#include "stdarg.h"

#ifdef WINDOWS
void error( const char* format, ... )
{
char buf[255]; // надеемся, что такой размер будет
// достаточен
va_list args;
va_start( args, format );
if
( vsprintf( buf, format, args ) " sizeof(buf) )
::MessageBox(NULL,buf, "*** ОШИБКА ***",
MB_OK | MB_ICONEXCLAMATION );
else
{
::MessageBox(NULL,
"
Переполнение буфера при печати сообщения об ошибке.",
"
Фатальная ошибка",
MB_OK | MB_ICONEXCLAMATION );
ExitProcess( -1 );
}
va_end( args );
}
#elif MOTIF
// Здесь следует функция обработки ошибки, используемая
// ОС Motif
#else
void error( const char* format, ... )
{
va_list args;
va_start( args, format );
vfprintf(stderr, format, args );
va_end ( args );
}
#endif



Часть

Препроцессор
Многие свойства языка Си++ делают препроцессор Си менее важным,
чем он был по традиции. Тем не менее, препроцессор иногда нужен даже
в программе на Си++ и, естественно, остается неотъемлемой частью
программирования на Си. Правила в этой главе книги посвящены
правильному использованию препроцессора.
Я должен сказать, что многие из тех макросов, которые мне пришлось
видеть, имеют довольно сомнительные основания с точки зрения
удобочитаемости и сопровождения. Я часто предпочитаю не пользоваться
макросом, если эквивалентная функция сделает ту же самую работу в
более удобочитаемой манере, и никогда не хочу использовать макрос,
вызывающий побочный эффект (будет рассмотрено ниже). В Си++ я
никогда не пользуюсь макросами с параметрами, используя вместо них
встроенные (inline) функции или шаблоны, которые расширяются
компилятором до встроенных функций. Макрос с параметрами даже в
языке Си должен быть методом, используемым лишь в крайнем случае. В
макросах трудно обнаруживать ошибки (так как они не выполняются в
пошаговом режиме), их трудно читать и, в лучшем случае, сложно
сопровождать. Используйте их лишь тогда, когда скорость выполнения
действительно является критерием, подтверждаемым фактическим
тестированием кода с их использованием. Таким образом, эта глава книги
содержит правила для тех случаев, где препроцессор является
единственным решением проблемы.


Препроцессор 105

79. Все из одного .h файла должно быть использовано
в, по меньшей мере, двух .c файлах
Это правило говорит само за себя - не загромождайте область
глобальных имен идентификаторами, которые не используются
глобально. Если идентификатор не используется вне текущего файла, то
он должен объявляться лишь в области действия текущего файла. Если
этот не используемый совместно идентификатор является глобальной
переменной или функцией, то он должен быть объявлен статическим.
Заметьте, что статические функции находят применение даже в Си++.
Имеется тенденция помещать все основные функции, используемые
любым из обработчиков сообщений, в собственно определение класса.
Иногда локальная статическая функция в файле .cpp делает эту работу
так же хорошо, и нет нужды загромождать прототипом этой функции
определение класса.

80. Используйте вложенные директивы #include
Хотя большинство из правил в этой главе говорят вам, как избежать
использования препроцессора, механизм включения файлов директивой
#include
является обязательной функцией препроцессора как в Си, так
и в Си++. Тем не менее, даже здесь существуют проблемы.
Это на самом деле плохая идея - требовать, чтобы кто-нибудь включал
файл, способный включать в себя следующий. Я всегда располагаю
директивы #include без определенного порядка или забываю вставить
одну из них. Следовательно, заголовочный файл должен всегда включать
те файлы, которые определяют то, что используется в текущем
заголовочном файле. Вследствие того, что могут возникнуть проблемы,
если компилятор прочитает какой-нибудь .h файл более одного раза, вы
должны предпринять шаги для предотвращения многократной обработки
одного и того же файла. Помещайте строки типа:

#ifndef FILENAME_H_
#define FILENAME_H_

в начале каждого заголовочного файла, и вставляйте соответственно:

#endif // FILENAME_H_

в конце. Так как константа FILENAME_H_ будет уже определена к
моменту второй попытки препроцессора обработать этот файл, то его
Правила программирования на Си и Си++

106


81. Вы должны быть всегда способны заменить
макрос функцией
Это вариант для макросов правила "не нужно неожиданностей (без
сюрпризов) ". Что-то, похожее внешне на функцию, должно действовать
подобно функции, даже если это на самом деле макрос. (По этой причине
я иногда предпочитаю записывать имя макроса заглавными буквами не
полностью, если его поведение сильно напоминает функцию. Хотя я
всегда использую все заглавные, если у макроса есть побочные эффекты).

При этом возникает насколько вопросов.
Во-первых, макрос не должен использовать переменные, не
передаваемые в качестве аргументов. Вот наихудший из возможных
способов в таких обстоятельствах:

Следующий код находится в заголовочном файле:

#define end() while(*p) \
++p

а этот - в файле .c:

char *f( char *str )
{
char *p = str;
end();
// ...
return p;
}

Здесь для сопровождающего программиста имеется несколько
неприятных сюрпризов. Во-первых, переменная p явно не используется,
поэтому появляется искушение стереть ее, разрушая этим код.
Аналогично, программа разрушится, если имя p будет заменено другим.
Наконец, будет большой неожиданностью то, что вызов end(), который
выглядит внешне как вызов обычной функции, будет модифицировать p.
Первая попытка урегулирования этой проблемы может выглядеть
следующим образом. В заголовочном файле:

#define end(p) while(*p) \
++p

и в файле .c:

char *f( char *str )
{
end(str);
// ...
return str;
}


Препроцессор 107

Но теперь макрос все еще необъяснимо модифицирует str, а нормальная
функция Си не может работать таким образом. (Функция Си++ может, но
не должна. Я объясню почему в той главе книги, которая посвящена
Си++). Для модификации строки str в функции вы должны передать в
нее ее адрес, поэтому то же самое должно быть применимо к макросу. Вот
третий (наконец-то правильный) вариант, в котором макрос end()
попросту заменен функцией с таким же именем. В заголовочном файле:

#define end(p) while(*(*p)) \
++(*p)

и в файле .c:

char *f( char *str )
{
end(&str);
// ...
return str;
}

Вместо end(&str) будет подставлено:

while
(*(*&p))
++(*&p)

и *&p - это то же самое, что и p, так как знаки * и & отменяют друг
друга - поэтому макрос в результате делает следующее:

while
(*(p))
++(p)

Вторая проблема с макросом в роли функции возникает, если вы желаете
выполнить в макросе больше, чем одно действие. Рассмотрим такой
макрос:

#define two_things() a();b()

if
( x )
two_things();
else
something_else();

который будет расширен следующим образом (тут я переформатировал,
чтобы сделать происходящее неприятно очевидным):

if ( x )
a();
b();
else
something_else();


Правила программирования на Си и Си++

108


Вы получаете сообщение об ошибке "у else отсутствует
предшествующий оператор if". Вы не можете решить эту проблему,
используя лишь фигурные скобки. Переопределение макроса следующим
образом:

#define two_things() { a(); b(); }

вызовет такое расширение:

if
( x )
{
a();
b();
}
;
else
something_else();

Эта вызывающая беспокойство точка с запятой - та, что следует после
two_things()
в вызове макроса. Помните, что точка с запятой сама по
себе является законным оператором в Си. Она ничего не делает, но она
законна. Вследствие этого else пытается связаться с этой точкой с
запятой, и вы получаете то же самое "у else отсутствует
предшествующий оператор if".
Не нужно говорить, что, несмотря на то, что макрос выглядит подобно
вызову функции, его вызов может не сопровождаться точкой с запятой. К
счастью, для этой проблемы имеется два настоящих решения. Первое из
них использует малоизвестный оператор "последовательного
вычисления" (или запятую):

#define two_things() ( a(), b() )

Эта запятая - та, что разделяет подвыражения в инициализирующей или
инкрементирующей частях оператора for. (Запятая, которая разделяет
аргументы функции, не является оператором последовательного
вычисления). Оператор последовательного вычисления выполняется
слева направо и получает значение самого правого элемента в списке (в
нашем случае значение, возвращаемое b()). Запись:

x = ( a(),b() );

означает прос

Список сторінок

Поділитися у соцмережах

☏ Замовлення реклами: +3805044-68-872

© Асоціація електронних бібліотек України

☝ Усі матеріали сайту (зокрема статті, зображення, рекламні оголошення тощо) призначені лише для попереднього ознайомлення. Усі права на публікації, представлені на сайті, належать їхнім законним власникам. Просимо Вас не зберігати копії інформації.