|   Лучшие приемы программирования на C
Шив Дутта, старший инженер-программист, IBM Стили и нормы программирования 
Необходимо использовать стиль программирования, который делает код читабельным и понятным. Несмотря на то, что некоторые разработчики имеют свой собственный стиль программирования или используют стиль программирования, принятый в их компании, хорошим тоном считается следовать стилю программирования Кернигана и Ритчи (Kernighan и Ritchie), используемому подавляющим большинством программистов на C. Однако, чересчур увлекшись, легко прийти к чему-нибудь такому: 
| 	int i;main(){for(;i["]<i;++i){--i;}"];read('-'-'-',i+++"hell\ 
	o, world!\n",'/'/'/'));}read(j,i,p){write(j/p+p,i---j,i/i); |  
 Dishonorable mention, Чемпионат по самому непонятному коду на C (Obfuscated C Code Contest), 1984 г. Автор кода неизвестен.Всегда в коде можно увидеть главную функцию, называемую main(). По стандарту ANSI эта функция определяется какint main(void)(если не нужно обрабатывать аргументы командной строки) или какint main( int argc, char **argv ). Не-ANSI компиляторы могут пропускать объявление void или составлять список имен переменных и следовать их объявлениям.Отступы 
Необходимо использовать вертикальные и горизонтальные отступы. Количество и расположение отступов и пробелов должно отражать структуру кода. Длинная строка условного оператора должна быть разбита на несколько строк. Например: 
 
| 	if (foo->next==NULL && number < limit 	    && limit <=SIZE
    	    && node_active(this_input)) {... |  будет лучше выглядеть как: 
 
|   	  if (foo->next == NULL
    	      && number < limit && limit <= SIZE
    	      && node_active(this_input))
        {
         ... |  Точно так же сложные циклы forдолжные быть разделены на несколько строк: 
| 	for (curr = *varp, trail = varp;
    	    curr != NULL;
   	    trail = &(curr->next), curr = curr->next )
	 {
           ... |  Другие сложные выражения, такие как использующие оператор ?:тоже лучше разделить на несколько строк: 
| 	z = (x == y)
    	    ? n + f(x)
    	     : f(y) - n;
    	          	     |  
Комментарии 
Комментарии должны описывать то, что происходит, каким образом это происходит, что означает тот или иной параметр, какие глобальные переменные используются, а также любые ограничения и возможные ошибки. Однако необходимо избегать необязательных комментариев. Если код понятен и используются хорошие имена переменных, то, возможно, не потребуется дополнительных пояснений. Так как комментарии не проверяются компилятором, то не гарантируется, что они правильные. Комментарии, которые не согласуются с кодом, вредны. Слишком большое число комментариев приводит к беспорядку в коде. Такой стиль комментирования является избыточным: 
 
| 	i=i+1;        /* добавляем 1 к i */
 |  Хорошо видно, что переменная iувеличивается на единицу. И еще более плохой вариант показать это так: 
|   		  /************************************
  		  *                                   *
          *        добавляем 1 к i  *
 		  *                                   *
   		   ************************************/
                    i=i+1; | Правила наименования 
Имена с ведущими или завершающими знаками подчеркивания предназначены только для системы целей и не должны использоваться для каких-либо пользовательских имен переменных. Правила определяют следующие требования: 
 
Константы #defineдолжны записываться ЗАГЛАВНЫМИ символами.Константы enumдолжны начинаться с заглавного символа или записываться полностью ЗАГЛАВНЫМИ символами.Слова function,typedefи имена переменных, так же как иstruct,unionиenumдолжны быть в нижнем регистре. Для понятности необходимо избегать имен, различающихся только регистром, например, fooиFoo. Точно так же лучше избегать одновременного использования именfoobarиfoo_bar. Необходимо избегать любых имен, которые похожи друг на друга. На многих клавиатурах и во многих шрифтахl,1иIвыглядят очень похоже. Переменная с именемl, в частности, плоха потому, что похожа на константу1.Имена переменных 
При выборе имени переменной не так важна длина имени, как понятность. Длинные имена могут назначаться глобальным переменным, которые редко используются, но индексы массивов, появляющиеся в каждой строке цикла, не должны быть значительно сложнее, чем i. Использованиеindexилиelementnumberне только усложняет набор, но и может сделать менее понятными детали вычислений. С длинными именами иногда сложнее понять, что именно происходит в коде. Легко сравнить: 
|      for(i=0 to 100)
          array[i]=0 |  и 
 
|      for(elementnumber=0 to 100)
          array[elementnumber]=0;
 | Имена функций 
Имена должны отражать то, что делают функции и что они возвращают. Функции используются в выражениях, часто в условных операторах if, поэтому они должны читаться соответственно. Например: непонятно, так как не говорит о том, возвращает ли функция TRUEпри прохождении проверки или наоборот; использование вместо этого: делает все понятным.Объявление переменных 
Все объявления внешних переменных должны предваряться ключевым словом extern. Обозначение указателя, *, должно сопровождать имя переменной, а не ее тип: вместо 
 Второй пример объявления переменных не является неправильным, но могут возникнуть сомнения из-за того, что tиuне объявлены как указатели.Заголовочные файлы 
Заголовочные файлы должны быть функционально организованы, т. е. объявления для различных подсистем должны содержаться в различных заголовочных файлах. Также объявления, которые являются платформозависимыми, должны быть вынесены в отдельный заголовочный файл. Следует избегать имен заголовочных файлов, совпадающих с именами стандартных библиотек. Строка #include "math.h''включает заголовочный файл стандартной библиотеки math, если файл с таким именем не будет найден в текущем каталоге. Если такое поведение - именно то, что нужно, то лучше оставить соответствующий комментарий. Наконец, использование абсолютных путей для заголовочных файлов - не самая лучшая идея. Опция компилятора C include-path(-Iна большинстве систем) - это предпочтительный метод обработки внешних библиотек и заголовочных файлов; он позволяет изменить структуру каталогов без необходимости изменения исходных кодов.scanfНе следует использовать scanfв серьезных приложениях. Обработка ошибок в этой функции неадекватна. Рассмотрим такой пример: 
| 	#include <stdio.h>
	int main(void)
	{
		int i;
		float f;
		printf("Enter an integer and a float: ");
		scanf("%d %f", &i, &f);
		printf("I read %d and %f\n", i, f);
		return 0;
	} |  Запустим тест:  Enter an integer and a float: 182 52.38 I read 182 and 52.380001 Теперь другой тест:  Enter an integer and a float: 6713247896 4.4 I read -1876686696 and 4.400000++и--При применении операций инкремента или декремента к переменной эта переменная не должна появляться в выражении более одного раза, так как итог в этом случае зависит от компилятора. Не следует писать код, который полагается на порядок обработки или особенности компилятора: 
 
| 	int i = 0, a[5];
	a[i] = i++;	/* присваивание значения  a[0]?  или  a[1]? */
 | Нельзя позволять себе видеть то, чего на самом деле нет 
Рассмотрим следующий пример: 
 
| 		while (c == '\t' // c = ' ' // c == '\n')
			c = getc(f);
 |  На первый взгляд такой оператор whileвыглядит корректным кодом на C. Однако использование оператора присваивания вместо оператора сравнения приводит к появлению синтаксически некорректного кода. Так как приоритет оператора=является наименьшим, то данное выражение будет интерпретировано следующим образом (скобки добавлены для наглядности): 
| 		while ((c == '\t' // c) = (' ' // c == '\n'))
			c = getc(f); |  Левая часть оператора присваивания: 
 не приводит к появлению корректного значения. Если переменная c содержит символ табуляции, то результат TRUEи дальнейшие вычисления не выполняются, аTRUEне может быть левой частью оператора присваивания.Явно выраженные намерения 
При написании кода, который может быть интерпретирован как что-то другое, необходимо заключать этот код в скобки, чтобы быть уверенным, что намерения выражены явно. Это поможет понять намерения разработчика при последующих обращениях к коду, а также помогает в сопровождении кода. Иногда можно разрабатывать код, который предупреждает возможные ошибки. Например, можно ставить константы в левую часть оператора сравнения, т. е. вместо: 
 
| 	while (c == '\t' // c == ' ' // c == '\n')
		c = getc(f); |  можно написать так: 
 
| 	while ('\t' == c // ' ' == c // '\n' == c)
		 c = getc(f); |  В этом случае компилятор выдаст предупреждение: 
 
| 	while ('\t' = c // ' ' == c // '\n' == c)
		 c = getc(f); |  Такой стиль программирования позволяет компилятору находить потенциальные проблемы; пример кода выше неправилен, так как пытается присвоить значение для \t.Ошибки из-за специфики реализации языка программирования 
Реализации языка C могут отличаться в некоторых аспектах. Необходимо иметь представление о той части языка, которая совпадает во всех реализациях. Зная это, значительно проще портировать программу на другую систему или другой компилятор, что уменьшает шансы столкнуться со спецификой компилятора. Для примера рассмотрим следующую строку: 
 Выражение будет интерпретироваться по правилу максимального оператора. Если комментарии могут быть вложенными, то интерпретация будет следующей: 
 Паре символов /*соответствует пара символов*/, поэтому выражение равно1. Если комментарии не вкладываются, то на некоторых системах/*в комментариях будет проигнорировано. На некоторых компиляторах будет также выдано предупреждение для вложенной последовательности/*. В любом случае выражение будет интерпретировано следующим образом: 2 * 1равно2.
Сбрасывание буфера на диск 
Когда приложение завершается некорректно, окончание его вывода обычно теряется. Приложение может не успеть полностью завершить вывод. Часть информации может оставаться в памяти и уже не будет записана в вывод. В некоторых системах такой незавершенный вывод может достигать нескольких страниц памяти. Потеря вывода может также привести к мысли, что программа завершилась ошибочно гораздо раньше, чем это произошло на самом деле. Способ решения такой проблемы состоит в организации принудительного вывода, особенно при отладке. Конкретная реализация этого отличается для различных систем, но обычно выглядит так: 
 
| 	setbuf(stdout, (char *) 0); |  Это выражение должно быть выполнено перед записью в stdout. В идеале этот код должен помещаться в начале функцииmain.getchar()- макрос или функция?Следующая программа выводит свои входные данные: 
 
| 	#include  <stdio.h>
	int main(void)
	{
		register int a;
		while ((a = getchar()) != EOF)
			putchar(a);
	}
 |  Если удалить включение заголовочного файла #include, то это вызовет ошибку компиляции, так как значениеEOFне объявлено. Программу можно переписать следующим образом: 
 
| 	#define EOF -1
	int main(void)
	{
		register int a;
		while ((a = getchar()) != EOF)
			putchar(a);
	}
 |  Это будет работать на большинстве систем, но на некоторых может быть значительно медленнее. Так как вызов функции обычно занимает довольно много времени, getcharчасто реализуют в виде макроса. Этот макрос определен вstdio.h, поэтому когда#include <stdio.h>удаляется, компилятор не знает, что такоеgetchar. На некоторых системах полагается, чтоgetchar- это функция, возвращающаяint. В действительности многие реализации компиляторов языка C имеют свои стандартные функции getchar, частично в целях защиты от таких оплошностей. Таким образом, ситуация, когда включение#include <stdio.h>пропущено, влечет использование компилятором собственной версии функцииgetchar. Дополнительные вызовы этой функции делают программу медленнее. То же самое верно и дляputchar.Пустой указатель 
Пустой указатель не указывает ни на какой объект. Неправильно использовать пустой указатель для любых целей, кроме присваивания и сравнения. Никогда не следует переопределять значение NULL, которое всегда должно равняться нулю. Пустой указатель любого типа должен всегда сравниваться с константным нулем, поскольку явное сравнение с переменной, имеющей значение нуль, или любой ненулевой константой будет платформозависимым. Переход по пустому указателю может вызвать странные эффекты.Что означает a+++++b?Единственный правильный способ интерпретации этого выражения такой: 
 Однако правило длинного оператора предписывает разбить выражение следующим образом: 
 Это синтаксически неверно, такой код эквивалентен строке: 
 Но результат a++не являетсяlvalueи, следовательно, не может быть операндом для++. Таким образом, правила для разрешения логических двусмысленностей не могут в этом примере привести к синтаксически верной конструкции. На практике, конечно, лучший способ избежать таких конструкций - это полная уверенность в том, что код интерпретируется однозначно. Конечно, добавление пробелов помогает компилятору понять цель оператора, но предпочтительнее (в перспективе сопровождения кода) разбить конструкцию на две строки:Осторожное обращение с функциями 
Функции обеспечивают наиболее общее структурирование кода на C. Они должны использоваться для решения проблемы "сверху вниз" - для разбиения задачи на ряд более мелких подзадач до тех пор, пока подзадача не будет легко решаться. Это помогает реализовать модульность и упростить документирование программы. Кроме того, программы, составленные из большого числа маленьких функций, значительно легче для отладки. Необходимо приводить все аргументы функций к нужному типу, если это не было сделано раньше, даже если точно известно, что компилятор осуществляет необходимое приведение типов. Делая приведение типа вручную, программист явно обозначает свои намерения и получит правильный результат при портировании приложения на другую платформу. Если заголовочные файлы не объявляют тип возвращаемого значения библиотечных функций, необходимо сделать это самостоятельно. Окружив объявления конструкцией #ifdef/#, можно упростить портирование своего кода на другую платформу. Прототипы функций используются для того, чтобы сделать код более устойчивым, а приложение - быстрым."Висячий" elseНужно опасаться проблемы "висячего" else, если нет полной уверенности в правильности конструкции: 
| 		if (a == 1)
			if (b == 2)
				printf("***\n");
			else
				printf("###\n");
 |  Правило гласит, что elseпринадлежит ближайшемуif. В случае, если возникают сомнения или потенциальная двусмысленность, то лучше добавить фигурные скобки для обозначения структуры кода.Границы массива 
Необходимо проверять границы всех массивов, включая строки, так как сегодняшнее fubar' может стать завтраfloccinaucinihilipilification. В надежном программном обеспечении не используетсяgets(). Тот факт, что в C элементы нумеруются с нуля, делает более вероятными ошибки подсчета. Однако требуются некоторые усилия на изучение того, как уберечься от них.Пустой оператор 
Пустой оператор цикла forилиwhileдолжен размещаться на отдельной строке и комментироваться так, чтобы было понятно, что в этом месте действительно пустой оператор, а не пропущенный код: 
| 	while (*dest++ = *src++)
   	 ;   /* VOID */
 | Проверка выражений на истинность 
Не нужно оставлять по умолчанию проверку на ненулевое значение, т. е.: 
 лучше, чем 
 даже если FAILимеет значение0, которое C рассматривает как ложь (конечно, здесь нужно соблюдать баланс с такими конструкциями как, например, показанная в разделе "Имена функций"). Явное значение поможет избежать ошибок, если вдруг кто-то решит, что при неудачном завершении должно возвращаться значение-1вместо0. Частые затруднения вызывает функция проверки равенства строк strcmp, так как нет единого значения, означающего, что строки неравны. Предпочтительный вариант - определение в этом случае макросаSTREQ: 
| 	#define STREQ(str1, str2) (strcmp((str1), (str2)) == 0)
 |  Использовать этот макрос можно в операторах следующего вида: 
 
| 	If ( STREQ( inputstring, somestring ) ) ...
 |  Таким образом, функция получает желаемое поведение (не требуется переписывать или переопределять стандартные библиотечные функции типа strcmp()). Не следует сравнивать логические выражения с 1(TRUE,YESи другими); вместо этого нужно проверять на равенство0(FALSE,NOи так далее). Большинство функций гарантируют возвращение0в случае неудачного завершения, и возвращение лишь ненулевого значения в случае удачного завершения. Таким образом, лучше переписать так: 
Вложенные операторы 
А сейчас - время для разговора о вложенном операторе присваивания. В некоторых конструкциях нет лучшего способа присваивания, хотя он и влечет увеличение кода в операторе и ухудшение читабельности: 
 
| 	while ((c = getchar()) != EOF) {
  	  process the character
	} |  Использование вложенного оператора присваивания для улучшения производительности возможно. Однако необходимо искать компромисс между увеличением скорости и усложнением сопровождения кода, которое возникает при использовании вложенных присваиваний в неподходящем месте. Например: 
 не должно заменяться на: 
 даже если последний вариант сможет сберечь один цикл. В долговременной перспективе разница во времени между двумя этими вариантами будет уменьшаться из-за использования компьютерной оптимизации, в то время как разница во времени, необходимом для сопровождения кода, будет увеличиваться.Оператор gotogotoнеобходимо использовать крайне умеренно. Один из случаев, когда этот оператор полезен - это необходимость прервать многоуровневый операторswitch,forилиwhile, хотя такая необходимость может свидетельствовать о том, что внутреннюю конструкцию лучше вынести в отдельный цикл.
 
| 	    for (...) {
      		while (...) {
    		  ...
                  if (wrong)
               	     goto error;
        
       	            }
		 }
    		...
            error:
   	       print a message
 |  Когда необходимо применять оператор goto, соответствующая метка перехода должна быть одна в строке и либо сдвинута на одну позицию табуляции влево от остального кода, либо располагаться в начале строки. В любом случае операторgotoи метка перехода должны иметь хороший комментарий по функциональности и цели использования."Проваливание" через switchКогда блок кода имеет несколько меток, каждую из них нужно размещать на отдельной строке. Этот элемент стиля программирования согласуется с правилом установки вертикальных отступов и делает перекомпоновку (если она понадобится) сравнений case простой задачей. Использование предоставляемой языком С возможности "проваливания" в операторе switchдолжно обязательно комментироваться в целях упрощения последующего сопровождения кода. Каждый, кто испытал на себе неприятности от ошибок при использовании этой возможности, знает, насколько это важно! 
| 	switch (expr) {
	case ABC:	
	case DEF:
    		statement;
    		break;
	case UVW:
	    	statement;	/*FALLTHROUGH*/	
	case XYZ:
    		statement;
  	  	break;	
	}
 |  Хотя последний оператор breakи не является необходимым, его использование предотвращает ошибку в случае, когда потребуется добавить еще одинcase. В случае, если используется вариантdefault, он должен быть последним и не требует оператораbreak.Константы 
Символические константы делают код более простым для чтения. Числовых констант, как правило, следует избегать; лучше использовать #defineдля задания понятного имени. Сосредоточение всех определений в одном месте (лучше всего - в заголовочном файле) также упрощает администрирование изменений в больших проектах, так как позволяет вносить изменения только в директивах#define. Можно рассматривать использование типа данных "перечисление" в качестве улучшенного способа объявления переменных, которые могут принимать только дискретные значения. Использование перечислений также позволяет компилятору выводить предупреждения при ошибках использования типа перечисления. И, наконец, явно перечисленные цифровые константы требуют меньше объяснений о своем происхождении при комментировании. Константы необходимо объявлять соответственно их использованию, т. е. необходимо указывать 540.0для числа с плавающей точкой вместо540с прямым объявлением типаfloat. Есть случаи, в которых константы0и1могут возникать явно вместо своих объявлений строковыми константами. Например, если циклforиндексирует массив, то код: 
| 	for (i = 0; i < arraysub; i++)
 |  оправдан, а код: 
 
| 	gate_t *front_gate = opens(gate[i], 7);
	if (front_gate == 0)
 	   error("can't open %s\n", gate[i]); |  - нет. Во втором примере front_gate- это указатель; когда значение является указателем, то оно должно сравниваться сNULL, а не с0. Даже простые значения типа1или0часто лучше воспринимаются в качествеTRUEиFALSE(илиYESиNO). Не нужно использовать переменные с плавающей точкой там, где нужны дискретные значения. Это связано с не совсем корректным представлением чисел с плавающей точкой (можно вспомнить второй пример из раздела scanfвыше). Сравнивать числа с плавающей точкой лучше используя<=или>=; явное сравнение (==или!=) может не обнаружить "достаточного" равенства. Символьные константы должны быть объявлены как символы, а не как числа. Нетекстовые символы являются более трудными для портирования. Если нетекстовые символы необходимы, в частности, при использовании в строках, они должны быть записаны в виде управляющих последовательностей из трех восьмеричных цифр, а не одной (например, '\007'). Даже в этом случае такое использование символов является платформозависимым и должно восприниматься таковым.Условная компиляция 
Условная компиляция полезна в случаях, когда требуется реализовать машинозависимый код, при отладке и для установок значений во время компиляции. Различные варианты управления могут легко привести к непредвиденным ситуациям. При использовании #ifdefдля машинозависимого кода необходимо быть уверенным, что если тип машины не определен, то возвращается сообщение об ошибке, а не используется конфигурация по умолчанию. Директива #error предназначена как раз для этих целей. При использовании#ifdefдля оптимизации лучше применять по умолчанию неоптимизированный код, чем некомпилируемый или некорректный. Необходимо тестировать неоптимизированный код. Разное
 
Утилиты для компиляции, такие как make, значительно упрощают задачу переноса приложения из одного окружения в другое. В процессе разработкиmakeперекомпилирует только те модули, которые были изменены со времени последней компиляции.Необходимо использовать lintкак можно чаще.lint- это тестер C-программ, который проверяет исходные файлы на языке C для обнаружения несовместимостей типов, расхождений между объявлениями функций и их вызовами, потенциальных ошибок в программе и тому подобного. Также необходимо изучить документацию компилятора и выяснить, какие опции сделают его более "разборчивым". Работа компилятора заключается в том, чтобы быть точным, поэтому необходимо дать ему возможность выдать отчет о потенциальных проблемах, используя соответствующие опции компиляции.Необходимо стараться минимизировать количество глобальных переменных. Один из выигрышей от этого заключается в уменьшении вероятности конфликтов с системными функциями. 
Многие программы завершаются некорректно, когда не получают ожидаемых входных данных. Все программы должны тестироваться на пустые входные данные. Это также поможет понять, как работает программа. 
Не следует полагать о пользователе или его поведении больше, чем этого требует программа. То, что "никогда не может произойти", иногда происходит. Надежная программа защищена от подобных случаев. Если есть непроверяемое граничное условие, то пользователь обязательно столкнется с ним! 
Никогда не нужно делать предположений о размере заданного типа данных, особенно указателей. При использовании в выражениях переменных типа charв большинстве реализаций компиляторов полагается этот тип данных как беззнаковое целое, но в некоторых - как знаковое. Поэтому разумнее каждый раз приводить этот тип данных к требуемому при использовании в арифметических выражениях. Не нужно полагаться на автоматическую инициализацию переменных и памяти, возвращаемой функцией malloc.Следует делать понятной структуру программы и ее цели. 
Необходимо всегда помнить, что разработчику в будущем может потребоваться модифицировать код или перенести его на другую платформу. Поэтому лучше сразу создавать код, который может быть легко портирован.  Заключение
 Общеизвестно, что сопровождение приложения отнимает значительную часть времени программиста. Частично это происходит из-за использования платформозависимых и нестандартных особенностей, но в большей степени - из-за плохого стиля программирования. В этой статье дается несколько советов, которые помогают сберечь время, требуемое для сопровождения кода. Следование этим советам сделает сопровождение приложений командой разработчиков более простым.  
 |