5.5 Не понимаю, почему нельзя использовать неизменяемые значения при
инициализации переменных и задании размеров массивов, как в следующем
примере:
const int n = 5;
int a[n];
О: Квалификатор const означает "только для чтения". Любой объект
квалифицированный как const, представляет собой нормальный объект,
существующий во время исполнения программы, которому нельзя присвоить другое
значение. Следовательно, значение такого объекта - это _не_
константное выражение в полном смысле этого слова. (В этом смысле С
не похож на С++). Если есть необходимость в истинных константах,
работающих во время компиляции, используйте препроцессорную директиву
#define.
Смотри: ANSI Разд. 3.4.
5.6 Какая разница между "char const *p" и "char * const p"?
О: "char const *p" - это указатель на постоянную литеру (ее нельзя
изменить); "char * const p" - это неизменяемый указатель на
переменную (ее можно менять ) типа char. Зарубите это себе на носу.
См. также 10.4.
Смотри: ANSI Разд. 3.5.4.1 .
5.7 Почему нельзя передать char ** функции, ожидающей const char **?
О: Можно использовать указатель-на-Т любых типов Т, когда ожидается
указатель-на-const-Т, но правило (точно определенное исключение из
него), разрешающее незначительные отличия в _указателях_, не может
применяться рекурсивно, а только на самом верхнем уровне.
Необходимо использовать точное приведение типов (т.е. в данном случае
(const char **)) при присвоении или передаче указателей, которые
имеют различия на уровне косвенной адресации, отличном от первого.
Смотри: ANSI Разд. 3.1.2.6 c. 26, Разд. 3.3.16.1 c. 54,
Разд. 3.5.3 c. 65.
5.8 Мой ANSI компилятор отмечает несовпадение, когда встречается с
декларациями
extern int func(float);
int func(x)
float x;
{...
О: Вы смешали декларацию в новом стиле "extern int func(float);"
с определением функции в старом стиле "int func(x) float x;".
Смешение стилей, как правило, безопасно (см. вопрос 5.9), но
только не в этом случае. Старый С ( и ANSI С при отсутствии
прототипов и в списках аргументов переменной длины) "расширяет"
аргументы определенных типов при передаче их функциям. Аргументы
типа float преобразуются в тип double, литеры и короткие целые
преобразуются в тип int. ( Если функция определена в старом стиле,
параметры автоматически преобразуются в теле функции к менее емким,
если таково их описание там.).
Это затруднение может быть преодолено либо с помощью определений
в новом стиле,
int func(float x) { ... }
либо с помощью изменения прототипа в новом стиле таким образом, чтобы
он соответствовал определению в старом стиле:
extern int func(double);
(В этом случае для большей ясности было бы желательно изменить и
определение в старом стиле так, чтобы параметр, если только не
используется его адрес, был типа double ).
Возможно, будет безопасней избегать типов char, short int, float для
возвращаемых значений и аргументов функций.
Смотри: ANSI Разд. 3.3.2.2 .
5.9 Можно ли смешивать определения функций в старом и новом стиле?
О: Смешение стилей абсолютно законно, если соблюдается осторожность
(обратите особое внимание на вопрос 5.8). Заметьте, однако, что
определение функций в старом стиле считается выходящим из
употребления, и в один прекрасный момент поддержка старого стиля
может быть прекращена.
5.10 Почему объявление
extern f(struct x {int s;} *p);
порождает невнятное предупреждение "struct x introduced in
prototype scope"? (структура объявлена в зоне видимости прототипа)?
О: В странном противоречии с обычными правилами для областей видимости
структура, объявленная только в прототипе, не может быть совместима с
другими структурами, объявленными в этом же файле. Более того,
вопреки ожиданиям тег структуры не может быть использован после
такого объявления (зона видимости объвления простирается до конца
прототипа). Для решения проблемы необходимо, чтобы прототипу
предшествовало "пустое" объявление
struct x;
, которое зарезервирует место в области видимости файла для
определения структуры x. Определение будет завершено объявлением
структуры внутри прототипа.
Смотри: ANSI Разд. 3.1.2.1 c. 21,Разд. 3.1.2.6 c. 26,
Разд. 3.5.2.3 c. 63.
5.11 У меня возникают странные сообщения об ошибках внутри кода,
"выключенного" с помощью #ifdef.
О: Согласно ANSI C, текст, "выключенный" с помощью #if, #ifdef, или
#ifndef должен состоять из "корректных единиц препроцессирования".
Это значит, что не должно быть незакрытых комментариев или
кавычек (обратите особое внимание, что апостроф внутри сокращенно
записанного слова смотрится как начало литерной константы).
Внутри кавычек не должно быть символов новой строки. Следовательно,
комментарии и псевдокод всегда должны находиться между
непосредственно предназначенными для этого символами начала и конца
комментария /* и */. (Смотрите, однако, вопросы 17.14 и 6.7).
Смотри: ANSI Разд. 2.1.1.2 c. 6, Разд. 3.1 c. 19 строка 37.
5.12 Могу я объявить main как void, чтобы прекратились раздражающие
сообщения "main return no value"? (Я вызываю exit(), так что
main ничего не возвращает).
О: Нет. main должна быть объявлена как возвращающая int и использующая
либо два , либо ни одного аргумента (подходящего типа). Если
используется exit(), но предупреждающие сообщения не исчезают,
Вам нужно будет вставить лишний return, или использовать, если
это возможно, директивы вроде "notreached".
Объявление функции как void просто не влияет на предупреждения
компилятора; кроме того, это может породить другую последовательность
вызова/возврата, несовместимую с тем, что ожидает вызывающая функция
(в случае main это исполняющая система языка С).
Смотри: ANSI Разд. 2.1.2.2.1 c. 7-8.
5.13: В точности ли эквивалентен возврат статуса с помощью exit(status)
возврату с помощью return?
О: Формально, да, хотя несоответсвия возникают в некоторых старых
нестандартных системах, в тех случаях, когда данные, локальные
для main(), могут потребоваться в процессе завершения выполнения
(может быть при вызовах setbuf() или atexit()), или при рекурсивном
вызове main().
Смотри: ANSI Разд. 2.1.2.2.3 c. 8.
5.14 Почему стандарт ANSI гарантирует только шесть значимых символов (при
отсутствии различия между прописными и строчными символами) для
внешних идентификаторов?
О: Проблема в старых компоновщиках, которые не зависят ни от стандарта
ANSI, ни от разработчиков компиляторов. Ограничение состоит в том,
что только первые шесть символов _значимы_, а не в том, что длина
идентификатора ограничена шестью символами. Это ограничение
раздражает, но его нельзя считать невыносимым. В Стандарте оно
помечено как "выходящее из употребления", так что в следующих
редакциях оно, вероятно, будет ослаблено.
Эту уступку современным компоновщикам, ограничивающим количество
значимых символов, обязательно нужно делать, не обращая внимания
на бурные протесты некоторых программистов. (В "Комментариях"
сказано, что сохранение этого ограничения было "наиболее болезненным".
Если Вы не согласны или надеетесь с помощью какого-то трюка заставить
компилятор, обремененный ограничивающим количество значимых символов
компоновщиком, понимать большее количество этих символов, читайте
превосходно написанный раздел 3.1.2 X3.159 "Комментариев" (см. вопрос
5.1), где обсуждается несколько такого рода подходов и объясняется,
почему эти подходы не могут быть узаконены.
Смотри: ANSI Разд. 3.1.2 c. 21, Разд. 3.9.1 c. 96, Rationale
Разд. 3.1.2 c. 19-21.
5.15 Какая разница между memcpy и memmove?
О: memmove гарантирует правильность операции копирования, если две
области памяти перекрываются. memcpy не дает такой гарантии и,
следовательно, может быть более эффективно реализована. В случае
сомнений лучше применять memmove.
Смотри: ANSI Разд. 4.11.2.1, 4.11.2.2, Rationale Разд.4.11.2 .
5.16 Мой компилятор не транслирует простейшие тестовые программы, выдавая
всевозможные сообщения об ошибках.
О: Видимо, Ваш компилятор разработан до приема стандарта ANSI и
поэтому не способен обрабатывать прототипы функций и тому подобное.
См. также вопросы 5.17 и 17.2.
5.17 Почему не определены некоторые подпрограммы из стандартной ANSI-
библиотеки, хотя у меня ANSI совместимый компилятор?
О: Нет ничего необычного в том, что компилятор, воспринимающий ANSI
синтаксис, не имеет ANSI-совместимых головных файлов или стандартных
библиотек. См. также вопросы 5.16 и 17.2.
5.18 Почему компилятор "Frobozz Magic C", о котором говорится, что он
ANSI-совместимый, не транслирует мою программу? Я знаю, что текст
подчиняется стандарту ANSI, потому что он транслируется компилятором
gcc.
О: Практически все компиляторы (а gcc - более других) поддерживают
некоторые нестандартные расширения. Уверены ли Вы, что отвергнутый
текст не применяет одно из таких расширений? Опасно экспериментировать
с компилятором для исследования языка. Стандарт может допускать
отклонения, а компилятор - работать неверно. См. также вопрос 4.4.
5.19 Почему мне не удаются арифметические операции с указателем типа
void * ?
О: Потому что компилятору не известен размер объекта, на который
указывает void *. Перед арифметическими операциями используйте
оператор приведения к типу (char *) или к тому типу, с которым
собираетесь работать. (Смотрите, однако, вопрос 2.18).
5.20 Правильна ли запись a[3]="abc"? Что это значит?
О: Эта запись верна в ANSI C (и, возможно, в некоторых более ранних
компиляторах), хотя полезность такой записи сомнительна. Объявляется
массив размера три, инициализируемый тремя буквами 'a','b',и 'c' без
завершающего стринг символа '\0'; Массив, следовательно, не может
использоваться как стринг функциями strcpy, printf %s и т.п.
Смотри: ANSI Разд. 3.5.7 c. 72-3.
5.21 Что такое #pragma и где это может пригодиться?
О: Директива #pragma обеспечивает особую, точно определенную "лазейку"
для выполнения зависящих от реализации действий: контроль за
листингом, упаковку структур, подавление предупреждающих сообщений
(вроде комментариев /* NOTREACHED */ старой программы lint) и т.п.
Смотри: ANSI Разд. 3.8.6 .
5.22 Что означает "#pragma once"? Я нашел эту директиву в одном из
головных файлов.
О: Это расширение, реализованное в некоторых препроцессорах, делает
головной файл идемпотентным, т.е. эффект от однократного включения
файла равен эффекту от многократного включения. Эта директива
приводит к тому же результату, что и прием с использованием #ifndef,
описанный в вопросе 6.4.
5.23 Вроде бы существует различие между зависимым от реализации,
неописанным(unspecified) и неопределенным (undefined) поведением.
В чем эта разница?
О: Если говорить кратко, то при зависимом от реализации поведении
необходимо выбрать один вариант и документировать его. При
неописанном поведении также выбирается один из вариантов, но в этом
случае нет необходимости это документировать. Неопределенное
поведение означает, что может произойти все что угодно. Ни в одном из
этих случаев Стандарт не выдвигает требований; в первых двух случаях
Стандарт иногда предлагает (а может и требовать) выбор из нескольких
близких вариантов поведения.
Если Вы заинтересованы в написании мобильных программ, можете
игнорировать различия между этими тремя случаями, поскольку всех их
необходимо будет избегать.
Смотри: ANSI Разд.1.6, особенно "Rationale".
6. Препроцессор С.
6.1 Как написать макрос для обмена любых двух значений?
О: На этот вопрос нет хорошего ответа. При обмене целых значений может
быть использован хорошо известный трюк с использованием исключающего
ИЛИ, но это не сработает для чисел с плавающей точкой или указателей.
Не годится этот прием и в случае, когда оба числа - на самом деле
одно и то же число. Из-за многих побочных эффектов (см. вопросы 4.1 и
4.2) не годится и "очевидное" суперкомпактное решение для целых чисел
a^=b^=a^=b. Когда макрос предназначен для переменных произвольного
типа (обычно так и бывает), нельзя использовать временную переменную,
поскольку не известен ее тип, а стандартный С не имеет оператора
typeof.
Если Вы не хотите передавать тип переменной третьим параметров, то,
возможно, наиболее гибким, универсальным решением будет отказ от
использования макроса.
6.2 У меня есть старая программа, которая пытается конструировать
идентификаторы с помощью макроса
#define Paste(a, b) a/**/b
но у меня это не работает.
О: То, что комментарий полностью исчезает, и, следовательно, может быть
использован для склеивания соседних лексем (в частности, для создания
новых идентификаторов), было недокументированной особенностью
некоторых ранних реализаций препроцессора, среди которых заметна
была реализация Рейзера (Reiser). Стандарт ANSI, как и K&R,
утверждает, что комментарии заменяются единичными пробелами. Но
поскольку необходимость склеивания лексем стала очевидной, стандарт
ANSI ввел для этого специальный оператор ##, который может быть
использован так:
#define Paste(a, b) a##b
Смотрите также вопрос 5.4.
Смотри: ANSI Разд. 3.8.3.3 c. 91, Rationale c. 66-7.
6.3 Как наилучшим образом написать cpp макрос, в котором есть несколько
инструкций?
О: Обычно цель состоит в том, чтобы написать макрос, который не отличался
бы по виду от функции. Это значит, что завершающая точка с запятой
ставится тем, кто вызывает макрос, а в самом теле макроса ее нет.
Тело макроса не может быть просто составной инструкцией, заключенной
в фигурные скобки, поскольку возникнут сообщения об ошибке
(очевидно, из-за лишней точки с запятой, стоящей после инструкции) в
том случае, когда макрос вызывается после if, а в инструкции if/else
имеется else-часть.
Обычно эта проблема решается с помощью такого определения:
#define Func() do { \
/* объявления */ \
что-то1; \
что-то2; \
/* ... */ \
} while(0) /* (нет завершающей ; ) */
Когда при вызове макроса добавляется точка с запятой, это расширение
становится простой инструкцией вне зависимости от контекста.
(Оптимизирующий компилятор удалит излишние проверки или
переходы по условию 0, хотя lint это может и не принять.)
Если требуется макрос, в котором нет деклараций или ветвлений, а
все инструкции - простые выражения, то возможен другой подход, когда
пишется одно, заключенное в круглые скобки выражение, использующее
одну или несколько запятых. (См. пример в вопросе 6.10. Такой подход
позволяет также реализовать "возврат" значения).
Смотри: CT&P Разд.6.3 c. 82-3.
6.4 Можно ли в головной файл с помощью #include включить другой
головной файл?
О: Это вопрос стиля, и здесь возникают большие споры. Многие полагают,
что "вложенных с помощью #include файлов" следует избегать:
авторитетный Indian Hill Style Guide (см. вопрос 14.3) неодобрительно
отзывается о таком стиле; становится труднее найти соответствующее
определение; вложенные #include могут привести к сообщениям о
многократном объявлении, если головной файл включен дважды; также
затрудняется корректировка управляющего файла для утилиты Make. С
другой стороны, становится возможным использовать модульный принцип
при создании головных файлов (головной файл включает с помощью
#include то, что необходимо только ему; в противном случае придется
каждый раз использовать дополнительный #include, что способно вызвать
постоянную головную боль); с помощью утилит, подобных grep (или файла
tags) можно легко найти нужные определения вне зависимости от того,
где они находятся, наконец, популярный прием:
#ifndef HEADERUSED
#define HEADERUSED
...содержимое головного файла...
#endif
делает головной файл "идемпотентным", то есть такой файл можно
безболезненно включать несколько раз; средства автоматической
поддержки файлов для утилиты Make (без которых все равно не обойтись
в случае больших проектов) легко обнаруживают зависимости при наличии
вложенных #include. См. также раздел 14.
6.5 Работает ли оператор sizeof при использовании средства препроцессора
#if?
О: Нет. Препроцессор работает на ранней стадии компиляции, до того как
становятся известны типы переменных. Попробуйте использовать
константы, определенные в файле <limits.h>, предусмотренном ANSI,
или "сконфигурировать" вместо этого командный файл. (А еще лучше
написать программу, которая по самой своей природе нечувствительна к
размерам переменных).
Смотри: ANSI Разд. 2.1.1.2 c. 6-7, Разд. 3.8.1 c. 87
примечание 83.
6.6 Можно ли с помощью #if узнать, как организована память машины -
по принципу: младший байт-меньший адрес или наоборот?
О: Видимо, этого сделать нельзя. (Препроцессор использует для
внутренних нужд только длинные целые и не имеет понятия об адресации).
А уверены ли Вы, что нужно точно знать тип организации памяти?
Уж лучше написать программу, которая от этого не зависит.
6.7 Во время компиляции мне необходимо сложное препроцесссирование, и я
никак не могу придумать, как это сделать с помощью cpp.
О: cpp не задуман как универсальный препроцессор. Чем заставлять cpp
делать что-то ему не свойственное, подумайте о написании небольшого
специализированного препроцессора. Легко раздобыть утилиту типа
make(1), которая автоматизирует этот процесс.
Если Вы пытаетесь препроцессировать что-то отличное от С,
воспользуйтесь универсальным препроцессором, (таким как m4).
6.8 Мне попалась программа, в которой, на мой взгляд, слишком много
директив препроцессора #ifdef. Как обработать текст, чтобы оставить
только один вариант условной компиляции, без использования cpp,
а также без раскрытия всех директив #include и #define?
О: Свободно распространяются программы unifdef, rmifdef и scpp, которые
делают в точности то, что Вам нужно. (См. вопрос 17.12).
6.9 Как получить список предопределенных идентификаторов?
О: Стандартного способа не существует, хотя необходимость возникает
часто. Если руководство по компилятору не содержит этих сведений, то,
возможно, самый разумный путь - выделить текстовые строки из
исполнимых файлов компилятора или препроцессора с помощью утилиты
типа strings(1) системы Unix. Имейте в виду, что многие зависящие от
системы предопределенные идентификаторы (например, "unix")
нестандартны (поскольку конфликтуют с именами пользователя) и поэтому
такие идентификаторы удаляются или меняются.
6.10 Как написать cpp макрос с переменным количеством аргументов?
О: Популярна такая уловка: определить макрос с одним аргументом, и
вызывать его с двумя открывающими и двумя закрывающими круглыми
скобками:
#define DEBUG(args) (printf("DEBUG: "), printf args)
if(n != 0) DEBUG(("n is %d\n", n));
Очевидный недостаток такого подхода в том, что нужно помнить о
дополнительных круглых скобках. Другие решения - использовать
различные макросы (DEBUG1, DEBUG2, и т.п.) в зависимости от
количества аргументов, или манипулировать запятыми:
#define DEBUG(args) (printf("DEBUG: "), printf(args))
#define _ ,
DEBUG("i = %d" _ i)
Часто предпочтительнее использовать настоящую функцию, которая
стандартным способом может использовать переменное число аргументов.
См. вопросы 7.1 и 7.2.
7. Списки аргументов переменной длины.
7.1 Как реализовать функцию с переменным числом аргументов?
О: Используйте головной файл <stdarg.h> (или, если необходимо, более
старый <varargs.h>).
Вот пример функции, которая объединяет произвольное количество
стрингов, помещая результат в выделенный с помощью malloc участок
памяти.
#include <stdlib.h> /* для malloc, NULL, size_t */
#include <stdarg.h> /* для va_ макросов */
#include <string.h> /* для strcat и т.п. */
char *vstrcat(char *first, ...)
{
size_t len = 0;
char *retbuf;
va_list argp;
char *p;
if(first == NULL)
return NULL;
len = strlen(first);
va_start(argp, first);
while((p = va_arg(argp, char *)) != NULL)
len += strlen(p);
va_end(argp);
retbuf = malloc(len + 1); /* +1 для \0 */
if(retbuf == NULL)
return NULL; /* ошибка */
(void)strcpy(retbuf, first);
va_start(argp, first);
while((p = va_arg(argp, char *)) != NULL)
(void)strcat(retbuf, p);
va_end(argp);
return retbuf;
}
Вызывается функция примерно так:
char *str = vstrcat("Hello, ", "world!", (char *)NULL);
Обратите внимание на явное приведение типа в последнем аргументе.
(Помните также, что после вызова такой функции нужно освободить
память).
Если компилятор разрабатывался до приема стандарта ANSI, перепишите
определение функции без прототипа ("char *vstrcat(first) char *first; {")
включите <stdio.h> вместо <stdlib.h>, добавьте
"extern char *malloc();" ,
и используйте int вместо size_t. Возможно, придется удалить приведение
(void) и использовать varargs.h вместо stdarg. Дополнительные
соображения смотрите в следующем вопросе.
Помните, что в прототипах функций со списком аргументов переменной
длины не указывается тип аргументов. Это значит, что по умолчанию
будет происходить "расширение" типов аргументов (см. вопрос 5.8).
Это также значит, что тип нулевого указателя должен быть явно указан
(см. вопрос 1.2).
Смотри: K&R II Разд. 7.3 c. 155, Разд. B7 c. 254; H&S
Разд. 13.4 c. 286-9; ANSI Разд 4.8 по 4.8.1.3 .
7.2 Как написать функцию, которая бы, подобно printf, получала строку
формата и переменное число аргументов, а затем для выполнения
большей части работы передавала бы все это printf?
О: Используйте vprintf, vfprintf, или vsprintf.
Перед Вами подпрограмма "error", которая после строки "error: "
печатает сообщение об ошибке и символ новой строки.
#include <stdio.h>
#include <stdarg.h>
void
error(char *fmt, ...)
{
va_list argp;
fprintf(stderr, "error: ");
va_start(argp, fmt);
vfprintf(stderr, fmt, argp);
va_end(argp);
fprintf(stderr, "\n");
}
Чтобы использовать старый головной файл <varargs.h> вместо <stdarg.h>,
измените заголовок функции
void error(va_alist)
va_dcl
{
char *fmt;
измените строку с va_start
va_start(argp);
и добавьте строку
fmt = va_arg(argp, char *);
между вызовами va_start и vfprintf. Заметьте, что после va_dcl нет
точки с запятой.
Смотри: K&R II Разд. 8.3 c. 174, Разд. B1.2 c. 245; H&S
Разд. 17.12 c. 337; ANSI Разд. 4.9.6.7, 4.9.6.8, 4.9.6.9 .
7.3 Как определить, сколько аргументов передано функции?
О: Для переносимых программ такая информация недоступна. Некоторые
старые системы имели нестандартную функцию nargs(), но ее полезность
всегда была сомнительна, поскольку обычно эта функция возвращает
количество передаваемых машинных слов, а не число аргументов.
(Структуры и числа с плавающей точкой обычно передаются в нескольких
словах).
Любая функция с переменным числом аргументов должна быть способна по
самим аргументам определить их число. Функции типа printf определяют
число аргументов по спецификаторам формата (%d и т.п.) в строке
формата (вот почему эти функции так скверно ведут себя при
несовпадении списка аргументов и строки формата). Другой общепринятый
прием - использовать признак конца списка (часто это числа 0,-1, или
нулевой указатель, приведенный к нужному типу).
Смотри примеры функций execl и vstrcat (вопросы 1.2 и 7.1).
7.4 Мне не удается добиться того, чтобы макрос va_arg возвращал аргумент
типа указатель-на-функцию.
О: Манипуляции с переписыванием типов, которыми обычно занимается
va_arg, кончаются неудачей, если тип аргумента слишком сложен -
вроде указателя на функцию. Если, однако, использовать typedef для
определния указателя на функцию, то все будет нормально.
Смотри: ANSI Разд. 4.8.1.2 c. 124.
7.5 Как написать функцию с переменным числом аргументов, которая передает
их другой функции с переменным числом аргументов?
О: В общем случае задача неразрешима. В качестве второй функции нужно
использовать такую, которая принимает указатель типа va_list, как это
делает vfprintf в приведенном выше примере. Если аргументы должны
передаваться непосредственно (a не через указатель типа va_list), и
вторая функция принимает переменное число аргументов (и нет
возможности создать альтернативную функцию, принимающую указатель
va_list), то создание переносимой программы невозможно. Проблема
может быть решена, если обратиться к языку ассемблера соответствующей
машины.
7.6 Как вызвать функцию со списком аргументов, создаваемым в процессе
выполнения?
О: Не существует способа, который бы гарантировал переносимость. Если
у Вас пытливый ум, раздобудьте редактор таких списков, в нем
есть несколько безрассудных идей, которые можно попробовать...
(См. также вопрос 16.11).
8. Булевы выражения и переменные.
8.1 Переменные какого типа правильнее использоваль как булевы?
Почему в языке С нет стандартного типа логических переменных?
Что использовать для значений true и false - #define или enum?
О: В языке С нет стандартного типа логических переменных, потому что
выбор конкретного типа основывается либо на экономии памяти, либо на
выигрыше времени. Такие вопросы лучше решать программисту
(использование типа int для булевой переменной может быть быстрее,
тогда как использование типа char экономит память).
Выбор между #define и enum - личное дело каждого, и споры о том, что
лучше, не особенно интересны (Но все же см. вопрос 9.1).
Используйте любой из четырех вариантов
#define TRUE 1 #define YES 1
#define FALSE 0 #define NO 0
enum bool {false, true}; enum bool {no, yes};
или последовательно в пределах программы или проекта используйте
числа 1 и 0.
(Возможно, задание булевых переменных через enum предпочтительнее,
если используемый Вами отладчик раскрывает содержимое
enum-переменных).
Некоторые предпочитают такие способы задания:
#define TRUE (1==1)
#define FALSE (!TRUE)
или задают "вспомогательный" макрос
#define Istrue(e) ((e) != 0)
Не видно, что они этим выигрывают ( см. вопрос 8.2 , а также
вопрос 1.6).
8.2 Разве не опасно задавать значение TRUE как 1, ведь в С любое не
равное нулю значение рассматривается как истинное? А если
оператор сравнения или встроенный булев оператор возвратит нечто,
отличное от 1?
О: Истинно (да-да!), что любое ненулевое значение рассматривается в
С как значение "ИСТИНА", но это применимо только "на входе", где
ожидается булева переменная. Когда булева переменная генерируется
встроенным оператором, гарантируется, что она равна 0 или 1.
Следовательно, сравнение
if((a == b) == TRUE)
как ни смешно оно выглядит, будет вести себя, как ожидается, если
значению TRUE соответствует 1. Как правило, явные проверки на TRUE
и FALSE нежелательны, поскольку некоторые библиотечные функции
(стоит упомянуть isupper,isalpha и т.п.), возвращают в случае успеха
ненулевое значение, которое _не обязательно_ равно 1. (Кроме того,
если Вы верите, что "if((a == b) == TRUE)" лучше чем "if(a == b)" ,
то почему не пойти дальше и не написать
"if(((a == b) == TRUE) == TRUE)"?
Хорошее "пальцевое" правило состоит в том, чтобы использовать TRUE и
FALSE (или нечто подобное) только когда булевым переменным или
аргументам функции присваиваются значения или когда значение
возвращается булевой функцией, но никогда при сравнении.
Макроопределения препроцессора TRUE и FALSE используются для большей
наглядности, а не потому, что конкретные значения могут измениться.
(См. также вопросы 1.7, 1.9).
Смотри: K&R I Разд. 2.7 c. 41; K&R II Разд. 2.6 c. 42,
Разд. A7.4.7 c. 204, Разд. A7.9 c. 206; ANSI Разд.
3.3.3.3, 3.3.8, 3.3.9, 3.3.13, 3.3.14, 3.3.15, 3.6.4.1, 3.6.5...
...Догонит ли Ахиллес и черепаху?
9. Структуры, перечисления и объединения.
9.1 Какова разница между enum и рядом директив препроцессора #define?
О: В настоящее время разница невелика. Хотя многие, возможно, предпочли
бы иное решение, стандарт ANSI утверждает, что произвольному числу
элементов перечисления могут быть явно присвоены целочисленные
значения. (Запрет на присвоение значений без явного приведения
типов, позволил бы при разумном использовании перечислений избежать
некоторых ошибок.)
Некоторые преимущества перечислений в том, что конкретные значения
задаются автоматически, что отладчик может представлять значения
перечислимых переменных в символьном виде, а также в том, что
перечислимые переменные подчиняются обычным правилам областей
действия. (Компилятор может также выдавать предупредения, когда
перечисления необдуманно смешиваются с целочисленными переменными.
Такое смешение может рассматриваться как проявление плохого стиля,
хотя формально это не запрещено). Недостаток перечислений в том, что
у программиста мало возможностей управлять размером переменных (и
предупреждениями компилятора тоже).
Смотри: K&R II Разд. 2.3 c. 39, Разд. A4.2 c. 196; H&S
Разд. 5.5 c. 100; ANSI Разд. 3.1.2.5, 3.5.2, 3.5.2.2 .
9.2 Я слышал, что структуры можно копировать как целое, что они могут
быть переданы функциям и возвращены ими, но в K&R I сказано, что
этого делать нельзя.
О: В K&R I сказано лишь, что ограничения на операции со структурами
будут сняты в следующих версиях компилятора; эти операции уже были
возможны в компиляторе Денниса Ритчи, когда издавалась книга K&R I.
Хотя некоторые старые компиляторы не поддерживают копирование
структур, все современные компиляторы имеют такую возможность,
предусмотренную стандартом ANSI C, так что не должно быть колебаний
при копировании и передаче структур функциям.
Смотри: K&R I Разд. 6.2 c. 121; K&R II Разд. 6.2 c. 129; H&S
Разд. 5.6.2 c. 103; ANSI Разд. 3.1.2.5, 3.2.2.1, 3.3.16 .
Читать 4 часть
Ссылки по теме