Автор: Крупник А.Б.. Дата публикации: 15.08.2004
Некоторые вопросы появляются вновь и вновь в этой конференции. Это
хорошие вопросы, и ответы на них могут быть далеко не очевидны, но каждый раз
ресурсы Сети и время читателя тратятся на повторяющиеся отклики и на нудные
поправки к некорректным ответам, возникновение которых неизбежно.
В этом документе, публикуемом ежемесячно, делается попытка ответить на
такие вопросы ясно и кратко, чтобы обсуждения стали более плодотворными, а
не возвращались постоянно к основным принципам.
Никакой список вопросов и ответов не заменит тщательного изучения
хорошего учебника или справочника по языку С. Тому, кто с интересом участвует
в этой конференции, должно быть также интересно прочесть одну или несколько
таких книг, желательно не один раз. Качество некоторых книг и руководств по
компилятору нельзя, к сожалению, назвать высоким; есть в них и попытки
увековечить некоторые мифы, которые данный документ пытается развеять.
Несколько заслуживающих внимания книг по С перечислены в библиографии.
Многие вопросы и ответы содержат ссылки на эти книги для дальнейшего изучения
интересующимся и увлеченным читателем. (Но помните о различной нумерации
документов ANSI и ISO стандартов С; см. вопрос 5.1).
Если у Вас есть вопрос, касающийся языка С, на который нет ответа в данном
документе, попытайтесь получить ответ на него из перечисленных здесь книг,
или спросите у знающих коллег, прежде чем использовать Сеть. Многие будут
счастливы ответить на ваши вопросы, но количество повторяющихся ответов на
один и тот же вопрос, как и возрастающее по мере привлечения читателей число
вопросов, может стать угнетающим. Если у Вас есть вопросы или замечания по
этому документу, используйте, пожалуйста, электронную почту -- эти вопросы и
ответы призваны снизить нагрузку на Сеть, а не увеличить ее.
Кроме списка наиболее часто задаваемых вопросов, в этом документе суммированы
наиболее часто даваемые ответы. Даже если Вы знаток языка С, полезно найти
время, чтобы бегло просмотреть этот документ, и тогда Вам не придется зря
тратить время в случае, когда кто-то поместил вопрос, ответ на который
уже дан.
Ответы на вопросы разбиты по темам:
1. Нулевые указатели
2. Указатели и массивы
3. Выделение памяти
4. Выражения
5. ANSI C
6. Препроцессор С
7. Списки аргументов переменной длины
8. Булевы выражения и переменные
9. Структуры, перечисления и объединения
10. Декларации
11. Cтандартный ввод/вывод
12. Библиотечные функции
13. Lint
14. Стиль
15. Операции с плавающей точкой
16. Интерфейс с операционной системой
17. Разное (Пребразование Fortran -> C , грамматики для YACC и т.п.)
К сему прилагаются часто задаваемые вопросы и ответы на них.
1. Нулевые указатели
1.1: Расскажите все-таки о пресловутых нулевых указателях.
О: Для каждого типа указателей существует (согласно определению языка)
особое значение - "нулевой указатель", которое отлично от всех других
значений и не указывает на какой-либо объект или функцию. Таким
образом, ни оператор &, ни успешный вызов malloc() никогда не
приведут к появлению нулевого указателя. (malloc возвращает нулевой
указатель, когда память выделить не удается, и это типичный пример
использования нулевых указателей как особых величин, имеющих
несколько иной смысл "память не выделена" или "теперь ни на что не
указываю". )
Нулевой указатель принципиально отличается от неинициализированного
указателя. Известно, что нулевой указатель не ссылается ни на какой
объект; неинициализированный указатель может ссылаться на что угодно.
См. также вопросы 3.1, 3.13, и 17.1.
В приведенном выше определении уже упоминалось, что существует
нулевой указатель для каждого типа указателя, и внутренние значения
нулевых указателей разных типов могут отличаться. Хотя программистам
не обязательно знать внутренние значения, компилятору всегда
необходима информация о типе указателя, чтобы различить нулевые
указатели, когда это нужно (см. ниже).
Смотри: K&R I Разд. 5.4 c. 97-8; K&R II Разд. 5.4 c. 102; H&S
Разд. 5.3 c. 91; ANSI Разд. 3.2.2.3 c. 38.
1.2: Как "получить" нулевой указатель в программе?
О: В языке С константа 0, когда она распознается как указатель,
преобразуется компилятором в нулевой указатель. То есть, если во
время инициализации, присваивания или сравнения с одной стороны
стоит переменная или выражение, имеющее тип указателя, компилятор
решает, что константа 0 с другой стороны должна превратиться в
нулевой указатель и генерирует нулевой указатель нужного типа.
Следовательно, следующий фрагмент абсолютно корректен:
char *p = 0;
if(p != 0)
Однако, аргумент, передаваемый функции, не обязательно будет
распознан как значение указателя, и компилятор может оказаться не
способным распознать голый 0 как нулевой указатель. Например,
системный вызов UNIX "execl" использует в качестве параметров
переменное количество указателей на аргументы, завершаемое нулевым
указателем. Чтобы получить нулевой указатель при вызове функции,
обычно необходимо явное приведение типов, чтобы 0 воспринимался как
нулевой указатель.
execl("/bin/sh", "sh", "-c", "ls", (char *)0);
Если не делать преобразования (char *), компилятор не поймет,
что необходимо передать нулевой указатель и вместо этого передаст
число 0. (Заметьте, что многие руководства по UNIX неправильно
объясняют этот пример.)
Когда прототипы функций находятся в области видимости, передача
аргументов идет в соответствии с прототипом и большинство приведений
типов может быть опущено, так как прототип указывает компилятору, что
необходим указатель определенного типа, давая возможность правильно
преобразовать нули в указатели. Прототипы функций не могут, однако,
обеспечить правильное преобразование типов в случае, когда функция
имеет список аргументов переменной длины, так что для таких
аргументов необходимы явные преобразования типов. Всегда безопаснее
явные преобразования в нулевой указатель, чтобы не наткнуться на
функцию с переменным числом аргументов или на функцию без прототипа,
чтобы временно использовать не-ANSI компиляторы, чтобы
продемонстрировать, что Вы знаете, что делаете. (Кстати, самое
простое правило для запоминания.)
Итог:
Можно использовать 0 Необходимо преобразование
инициализация вызов функции, прототип которой
вне области видимости
присваивание переменное число аргументов
при вызове функции
сравнение
вызов функции, прототип
в области видимости,
количество аргументов
фиксировано
Смотри: K&R I Разд. A7.7 c. 190, Разд. A7.14 c. 192; K&R II
Разд. A7.10 c. 207, Разд. A7.17 c. 209; H&S Разд. 4.6.3 c. 72; ANSI
Разд. 3.2.2.3 .
1.3 Что такое NULL и как он определен с помощью #define?
O: Многим программистам не нравятся нули, беспорядочно разбросанные по
программам. По этой причине макрос препроцессора NULL определен в
<stdio.h> или <stddef.h> как значение 0 (или (void *) 0, об этом
значении поговорим позже.) Программист, который хочет явно различать
0 как целое и 0 как нулевой указатель может использовать NULL в тех
местах, где необходим нулевой указатель. Это только стилистическое
соглашение; препроцессор преобразует NULL опять в 0, который затем
распознается компилятором в соответствующем контексте как нулевой
указатель. В отдельных случаях при передаче параметров функции,
может все же потребоваться явное указание типа перед NULL (как и
перед 0). (Таблица в вопросе 1.2 приложима как к NULL, так и к 0).
NULL нужно использовать _только_ для указателей; см. вопрос 1.8.
Смотри: K&R I Разд. 5.4 c. 97-8; K&R II Разд. 5.4 c. 102; H&S
Разд. 13.1 c. 283; ANSI Разд. 4.1.5 c. 99, Разд. 3.2.2.3 c. 38,
Rationale Разд. 4.1.5 c. 74.
1.4: Как #define должен определять NULL на машинах, использующих
ненулевой двоичный код для внутреннего представления нулевого
указателя?
О: Программистам нет необходимости знать внутреннее представление(я)
нулевых указателей, ведь об этом обычно заботится компилятор.
Если машина использует ненулевой код для представления нулевых
указателей, на совести компилятора генерировать этот код, когда
программист обозначает нулевой указатель как "0" или NULL.
Следовательно, определение NULL как 0 на машине, для которой нулевые
указатели представляются ненулевыми значениями так же правомерно как
и на любой другой, так как компилятор должен (и может) генерировать
корректные значения нулевых указателей в ответ на 0, встретившийся в
соответствующем контексте
1.5 Пусть NULL был определен следующим образом:
#define NULL ((char *)0).
Ознает ли это, что функциям можно передавать NULL без преобразования
типа?
О: В общем, нет. Проблема в том, что существуют компьютеры, которые
используют различные внутренние представления для указателей на
различные типы данных. Предложенное определение через #define
годится, когда функция ожидает в качестве передаваемого параметра
указатель на char, но могут возникнуть проблемы при передаче
указателей на переменные других типов, а верная конструкция
FILE *fp = NULL;
может не сработать.
Тем не менее, ANSI C допускает другое определение для NULL:
#define NULL ((void *)0)
Кроме помощи в работе некорректным программам (но только в случае
машин, где указатели на разные типы имеют одинаковые размеры, так
что помощь здесь сомнительна) это определение может выявить
программы, которые неверно используют NULL (например, когда был
необходим символ ASCII NUL; см. вопрос 1.8).
Смотри: ANSI Rationale Разд. 4.1.5 c. 74.
1.6: Я использую макрос
#define Nullptr(type) (type *)0 ,
который помогает задавать тип нулевого указателя.
О: Хотя этот трюк и популярен в определенных кругах, он стоит немного.
Он не нужен при сравнении и присваивании; см. вопрос 1.2. Он даже
не экономит буквы. Его использование показывает тому, кто читает
программу, что автор здорово "сечет" в нулевых указателях, и требует
гораздо более аккуратной проверки определения макроса, его
использования и _всех_ остальных случаев применения указателей.
См. также вопрос 8.1.
1.7: Корректно ли использовать сокращенный условный оператор if(p) для
проверки того, что указатель ненулевой? А что если внутреннее
представление для нулевых указателей отлично от нуля?
О: Когда С требует логическое значение выражения (в инструкциях if,
while, for, и do и для операторов &&, ||, !, и ?:) значение false
получается, когда выражение равно нулю, а значение true получается в
противоположном случае. Таким образом, если написано
if(expr)
где "expr" - произвольное выражение, компилятор на самом деле
поступает так, как будто было написано
if(expr != 0)
Подставляя тривиальное выражение, содержащее указатель "p" вместо
"expr", получим
if(p) эквивалентно if(p != 0)
и это случай, когда происходит сравнение, так что компилятор поймет,
что неявный ноль - это нулевой указатель и будет использовать
правильное значение. Здесь нет никакого подвоха, компиляторы работают
именно так и генерируют в обоих случаях идентичный код. Внутреннее
представление указателя _не_ имеет значения.
Оператор логического отрицания ! может быть описан так:
!expr на самом деле эквивалентно expr?0:1
Читателю предлагается в качестве упражнения показать, что
if(!p) эквивалентно if(p == 0)
Хотя "сокращения" типа if(p) совершенно корректны, кое-кто считает
их использование дурным стилем.
См. также вопрос 8.2.
Смотри: K&R II Разд. A7.4.7 c. 204; H&S Разд. 5.3 c. 91; ANSI
Разд. 3.3.3.3, 3.3.9, 3.3.13, 3.3.14, 3.3.15, 3.6.4.1, и
3.6.5 .
1.8: Если "NULL" и "0" эквивалентны, то какую форму из двух использовать?
О: Многие программисты верят, что "NULL" должен использоваться во всех
выражениях, содержащих указатели как напоминание о том, что значение
должно рассматриваться как указатель. Другие же чувствуют, что
путаница, окружающая "NULL" и "0", только усугубляется, если "0"
спрятать в операторе #define и предпочитают использовать "0" вместо
"NULL". Единственного ответа не существует. Программисты на С должны
понимать, что "NULL" и "0" взаимозаменяемы и что "0" без
преобразования типа можно без сомнения использовать при
инициализации, присваивании и сравнении. Любое использование "NULL"
(в противоположность "0" ) должно рассматриваться как ненавязчивое
напоминание, что используется указатель; программистам не нужно
ничего делать (как для своего собственного понимания, так и для
компилятора) для того, чтобы отличать нулевые указатели от целого
числа 0. NULL _нельзя_ использовать, когда необходим другой тип нуля.
Даже если это и будет работать, с точки зрения стиля
программирования это плохо.(ANSI позволяет определить NULL с помощью
#define как (void *)0. Такое определение не позволит использовать
NULL там, где не подразумеваются указатели). Особенно не рекомендуется
использовать NULL там, где требуется нулевой код ASCII (NUL). Если
необходимо, напишите собственное определение
#define NUL '\0'
Смотри: K&R II Разд. 5.4 c. 102.
1.9 Но не лучше ли будет использовать NULL (вместо 0) в случае, когда
значение NULL изменяется, быть может, на компьютере с ненулевым
внутренним представлением нулевых указателей?
О: Нет. Хотя символические константы часто используются вместо чисел
из-за того, что числа могут измениться, в данном случае причина, по
которой используется NULL, иная. Еще раз повторим: язык гарантирует,
что 0, встреченный там, где по контексту подразумевается указатель,
будет заменен компилятором на нулевой указатель. NULL используется
только с точки зрения лучшего стиля программирования.
1.10: Я в растерянности. Гарантируется, что NULL равен 0, а нулевой
указатель нет?
О: Термин "null" или "NULL" может не совсем обдуманно использоваться
в нескольких смыслах:
1. Нулевой указатель как абстрактное понятие языка, определенное
в вопросе 1.1. Он представляется с помощью...
2. Внутреннее (на стадии выполнения) представление нулевого
указателя, которое может быть отлично от нуля и различаться
для различных типов указателей. О внутреннем представлении
нулевого указателя должны заботиться только создатели
компилятора. Программистам на С это представление не
известно, поскольку они используют...
3. Синтаксическое соглашение для нулевых указателей, символ
"0". Вместо него часто используют...
4. Макрос NULL который с помощью #define определен как "0"
или "(void *)0". Наконец, нас может запутать...
5. Нулевой код ASCII (NUL), в котором все биты равны нулю, но
который имеет мало общего с нулевым указателем, разве что
названия похожи; и...
6. "Нулевой стринг", или, что то же самое, пустой стринг ("").
Термин "нулевой стринг" может приводить к путанице в С и,
возможно, его следует избегать, так как пустой стринг
включает символ '\0', но не нулевой указатель, и здесь мы
уже идем по кругу...
В этом документе фраза "нулевой указатель" (прописными буквами)
используется в смысле 1, символ "0" в смысле 3, а слово "NULL",
записанное большими буквами, в смысле 4.
1.11: Почему так много путаницы связано с нулевыми указателями? Почему
так часто возникают вопросы?
О: Программисты на С традиционно хотят знать больше, чем это необходимо
для программирования, о внутреннем представлении кода. Тот факт,
что внутреннее представление нулевых указателей для большинства
машин совпадает с их представлением в исходном тексте, т.е. нулем,
способствует появлению неверных обобщений. Использование макроса
(NULL) предполагает, что значение может впоследствии измениться, или
иметь другое значение для какого-нибудь компьютера. Конструкция
"if(p == 0)" может быть истолкована неверно, как преобразование перед
сравнением p к целому типу, а не 0 к типу указателя. Наконец, часто
не замечают, что термин "null" употребляется в разных смыслах
(перечисленных выше).
Хороший способ устранить путаницу - вообразить, что язык С имеет
ключевое слово (возможно, nil, как в Паскале), которое обозначает
нулевой указатель. Компилятор либо пребразует "nil" в нулевой
указатель нужного типа, либо сообщает об ошибке, когда этого сделать
нельзя. На самом деле, ключевое слово для нулевого указателя в С
- это не "nil" а "0". Это ключевое слово работает всегда, за
исключением случая, когда компилятор воспринимает в неподходящем
контексте "0" без указания типа как целое число, равное нулю, вместо
того, чтобы сообщить об ошибке. Программа может не работать, если
предполагалось, что "0" без явного указания типа - это нулевой
указатель.
1.12: Я все еще в замешательстве. Мне так и не понятна возня с нулевыми
указателями.
О: Следуйте двум простым правилам:
1. Для обозначения в исходном тексте нулевого указателя,
используйте "0" или "NULL".
2. Если "0" или "NULL" используются как фактические
аргументы при вызове функции, приведите их к типу указателя,
который ожидает вызываемая функция.
Остальная часть дискуссии посвящена другим заблуждениям, связанным с
нулевыми указателями, внутреннему представлению нулевых указателей
(которое Вам знать не обязательно), а также усовершенствованиям
стандарта ANSI C. Изучите ответы на вопросы 1.1, 1.2, и 1.3, а
также учтите вопросы 1.8 и 1.11, и все будет нормально.
1.13: Учитывая всю эту путаницу, связанную с нулевыми указателями, не лучше
ли просто потребовать, чтобы их внутреннее представление было нулевым?
О: Если причина только в этом, то поступать так было бы неразумно, так
как это неоправданно ограничит конкретную реализацию, которая (без
таких ограничений) будет естественным образом представлять нулевые
указатели специальными, отличными от нуля значениями, особенно когда
эти значения автоматически будут вызывать специальные аппаратные
прерывания, связанные с неверным доступом.
Кроме того, что это требование даст на практике? Понимание нулевых
указателей не требует знаний о том, нулевое или ненулевое их
внутреннее представление. Предположение о том, что внутреннее
представление нулевое, не приводит к упрощению кода (за исключением
некоторых случаем сомнительного использования calloc; см. вопрос
3.13). Знание того, что внутреннее представление равно нулю, не
упростит вызовы функций, так как _размер_ указателя может быть
отличным от размера указателя на int. (Если вместо "0" для
обозначения нулевого указателя использовать "nil" (см. вопрос 1.11),
необходимость в нулевом внутреннем представлении нулевых указателей
даже бы не возникла).
1.14: Ну а если честно, на какой-нибудь реальной машине используются
ненулевые внутренние представления нулевых указателей или разные
представления для указателей разных типов?
О: Серия Prime 50 использует сегмент 07777, смещение 0 для нулевого
указателя, по крайней мере, для PL/I. Более поздние модели используют
сегмент 0, смещение 0 для нулевых указателей С, что делает
необходимыми новые инструкции, такие как TCNP (проверить нулевой
указатель С), которые вводятся для совместимости с уцелевшими
скверно написанными С программами, основанными на неверных
предположениях. Старые машины Prime с адресацией слов были печально
знамениты тем, что указатели на байты (char *) у них были большего
размера, чем указатели на слова (int *).
Серия Eclipse MV корпорации Data General имеет три аппаратно
поддерживаемых типа указателей (указатели на слово, байт и бит), два
из которых - char * и void * используются компиляторами С. Указатель
word * используется во всех других случаях.
Некоторые центральные процессоры Honeywell-Bull используют код 06000
для внутреннего представления нулевых указателей.
Серия CDC Cyber 180 использует 48-битные указатели, состоящие из
кольца (ring), сегмента и смещения. Большинство пользователей
(в кольце 11) имеют в качестве нулевых указателей код 0xB00000000000.
Символическая Лисп-машина с теговой архитектурой даже не имеет
общеупотребительных указателей; она использует пару <NIL,0> (вообще
говоря, несуществующий <объект, смещение> хендл) как нулевой
указатель С.
В зависимости от модели памяти, процессоры 80*86 (PC) могут
использовать либо 16-битные указатели на данные и 32-битные указатели
на функции, либо, наоборот, 32-битные указатели на данные и 16-битные
- на функции.
Старые модели HP 3000 используют различные схемы адресации для байтов
и для слов. Указатели на char и на void, имеют, следовательно,
другое представление, чем указатели на int (на структуры и т.п.),
даже если адрес одинаков.
1.15 Что означает ошибка во время исполнения "null pointer assignment"
(запись по нулевому адресу). Как мне ее отследить?
О: Это сообщение появляется только в системе MS-DOS (см., следовательно,
раздел 16) и означает, что произошла запись либо с помощью
неинициализированного, либо нулевого указателя в нулевую область.
Отладчик обычно позволяет установить точку останова при доступе к
нулевой области. Если это сделать нельзя, Вы можете скопировать около
20 байт из области 0 в другую и периодически проверять, не изменились
ли эти данные.
2. Указатели и массивы
2.1: В одном файле у меня есть описание char a[6] а в другом я объявил
extern char *a. Почему это не работает?
О: Декларация extern char *a просто не совпадает с текущим определением.
Тип "Указатель-на-тип-Т" не равен типу "массив-типа-Т". Используйте
extern char a[].
2.2: Но я слышал, что char a[] эквивалентно char *a.
О: Ничего подобного. (То, что Вы слышали, касается формальных параметров
функций, см. вопрос 2.4.) Массивы - не указатели. Объявление массива
"char a[6];" требует определенного места для шести символов, которое
будет известно под именем "a". То есть, существует место под именем
"a", в которое могут быть помещены 6 символов. С другой стороны,
объявление указателя "char *p;" требует места только для самого
указателя. Указатель будет известен под именем "p" и может указывать
на любой символ (или непрерывный массив символов).
Как обычно, лучше один раз увидеть, чем сто раз услышать.
Объявление
char a[] = "hello";
char *p = "world";
породит структуры данных, которые могут быть представлены так:
+---+---+---+---+---+---+
a: | h | e | l | l | o |\0 |
+---+---+---+---+---+---+
+-----+ +---+---+---+---+---+---+
p: | *======> | w | o | r | l | d |\0 |
+-----+ +---+---+---+---+---+---+
Важно понимать, что ссылка типа х[3] порождает разный код в
зависимости от того, массив х или указатель. Если взять приведенную
выше декларацию, то, когда компилятор встречается с выражением а[3],
он генерирует код, позволяющий переместиться к месту под именем "a",
перемещается на три символа вперед и затем читает требуемый символ.
В случае выражения p[3] компилятор генерирует код, чтобы начать с
позиции "p", считывает значение указателя, прибавляет к указателю
3 и, наконец, читает символ, на который указывает указатель. В нашем
примере и a[3] и p[3] оказались равны 'l', но компилятор получает
этот символ по-разному. (Смотри также вопросы 17.19 и 17.20.)
2.3: Тогда что же понимается под "эквивалентностью указателей и массивов"
в С?
О: Большая часть путаницы вокруг указателей в С происходит от
непонимания этого утверждения. "Эквивалентность" указателей и
массивов не позволяет говорить не только об идентичности, но и
взаимозаменяемости.
"Эквивалентность" относится к следующему ключевому определению:
значение [см. вопрос 2.5] типа массив-Т, которое появляется
в выражении, превращается (за исключением трех случаев) в
указатель на первый элемент массива; тип результирующего
указателя - указатель-на-Т.
(Исключение составляют случаи, когда массив оказывается операндом
sizeof, оператора & или инициализатором символьной строки для
массива литер.)
Вследствие этого определения нет заметной разницы в поведении
оператора индексирования [], если его применять к массивам и
указателям. Согласно правилу, приведенному выше, в выражении типа
а[i] ссылка на массив "a" превращается в указатель и дальнейшая
индексация происходит так, как будто существует выражение с
указателем p[i] (хотя доступ к памяти будет различным, как описано в
ответе на вопрос 2.2). В любом случае выражение x[i], где х - массив
или указатель) равно по определению *((x)+(i)).
Смотри: K&R I Разд.5.3 c.93-6; K&R II Разд.5.3 c. 99; H&S
Разд.5.4.1 c. 93; ANSI Разд.3.2.2.1, Разд.3.3.2.1,
Разд.3.3.6 .
2.4: Тогда почему объявления указателей и массивов взаимозаменяемы в
в качестве формальных параметров?
О: Так как массивы немедленно превращаются в указатели, массив на самом
деле не передается в функцию. По общему правилу, любое похожее на
массив объявление параметра
f(a)
char a[];
рассматривается компилятором как указатель, так что если был передан
массив, функция получит:
f(a)
char *a;
Это превращение происходит только для формальных параметров функций,
больше нигде. Если это превращение раздражает Вас, избегайте его;
многие пришли к выводу, что порождаемая этим путаница перевешивает
небольшое преимущество от того, что объявления смотрятся как вызов
функции и/или напоминают о том, как параметр будет использоваться
внутри функции.
Смотри: K&R I Разд.5.3 c. 95, Разд.A10.1 c. 205; K&R II
Разд.5.3 c. 100, Разд.A8.6.3 c. 218, Разд.A10.1 c.226; H&S
Разд.5.4.3 c. 96; ANSI Разд.3.5.4.3, Разд.3.7.1, CT&P Разд.3.3
c. 33-4.
2.5 Как массив может быть значением типа lvalue, если нельзя присвоить
ему значение?
О: Стандарт ANSI C определяет "модифицируемое lvalue", но массив к
этому не относится.
Смотри: ANSI Разд. 3.2.2.1 c. 37.
2.6 Почему sizeof неправильно определяет размер массива, который передан
функции в качестве параметра?
О: Оператор sizeof сообщает размер указателя, который на самом деле
получает функция. (см. вопрос 2.4).
2.7 Кто-то объяснил мне, что массивы это на самом деле только постоянные
указатели.
О: Это слишком большое упрощение. Имя массива - это константа,
следовательно, ему нельзя присвоить значение, но массив - это не
указатель, как должно быть ясно из ответа на вопрос 2.2 и из
картинки, помещенной там же.
2.8 C практической точки зрения в чем разница между массивами и
указателями?
О: Массивы автоматически резервируют память, но не могут изменить
расположение в памяти и размер. Указатель должен быть задан так,
чтобы явно указывать на выбранный участок памяти (возможно с помощью
malloc), но он может быть по нашему желанию переопределен (т.е. будет
указывать на другие объекты) и, кроме того, указатель имеет много
других применений, кроме службы в качестве базового адреса блоков
памяти.
В рамках так называемой эквивалентности массивов и указателей (см.
вопрос 2.3), массивы и указатели часто оказываются взаимозаменяемыми.
Особенно это касается блока памяти, выделенного функцией malloc,
указатель на который часто используется как настоящий массив. (На
этот блок памяти можно ссылаться, используя [], cм. вопрос 2.14,
а также вопрос 17.20.)
2.9 Я наткнулась на шуточный код, содержащий "выражение" 5["abcdef"].
Почему такие выражения возможны в С?
О: Да, Вирджиния, индекс и имя массива можно переставлять в С. Этот
забавный факт следует из определения индексации через указатель, а
именно, a[e] идентично *((a)+(e)), для _любого_ выражения е и
основного выражения а, до тех пор пока одно из них будет указателем,
а другое целочисленным выражением. Это неожиданная коммутативность
часто со странной гордостью упоминается в С-текстах, но за пределами
Соревнований по Непонятному Программированию (Obfuscated C Contest)
она применения не находит. (см. вопрос 17.13).
Смотри: ANSI Rationale Разд. 3.3.2.1 c. 41.
2.10 Мой компилятор ругается, когда я передаю двумерный массив функции,
ожидающей указатель на указатель.
О: Правило, по которому массивы превращаются в указатели не может
применяться рекурсивно. Массив массивов (т.е. двумерный массив в С)
превращается в указатель на массив, а не в указатель на указатель.
Указатели на массивы могут вводить в заблуждение и применять их нужно
с осторожностью. (Путаница еще более усугубляется тем, что существуют
некорректные компиляторы, включая некоторые версии pcc и полученные
на основе pcc программы lint, которые неверно вопринимают присваивание
многоуровневым указателям многомерных массивов.) Если вы передаете
двумерный массив функции:
int array[NROWS][NCOLUMNS];
f(array);
описание функции должно соответствовать
f(int a[][NCOLUMNS]) {...}
или
f(int (*ap)[NCOLUMNS]) {...} /* ap - указатель на массив */
В случае, когда используется первое описание, компилятор неявно
осуществляет обычное преобразование "массива массивов" в "указатель
на массив"; во втором случае указатель на массив задается явно.
Так как вызываемая функция не выделяет место для массива, нет
необходимости знать его размер, так что количество "строк" NROWS
может быть опущено. "Форма" массива по-прежнему важна, так что
размер "столбца" NCOLUMNS должен быть включен (а для массивов
размерности 3 и больше, все промежуточные размеры).
Если формальный параметр функции описан как указатель на указатель,
то передача функции в качестве параметра двумерного массива будет,
видимо, некорректной.
Смотри: K&R I Разд.5.10 c. 110; K&R II Разд.5.9 c. 113.
2.11: Как писать функции, принимающие в качестве параметра двумерные
массивы, "ширина" которых во время компиляции неизвестна?
О: Это непросто. Один из путей - передать указатель на элемент [0][0]
вместе с размерами и затем симулировать индексацию "вручную":
f2(aryp, nrows, ncolumns)
int *aryp;
int nrows, ncolumns;
{ ... array[i][j] это aryp[i * ncolumns + j] ... }
Этой функции массив из вопроса 2.10 может быть передан так:
f2(&array[0][0], NROWS, NCOLUMNS);
Нужно, однако, заметить, что программа, выполняющая индексирование
многомерного массива "вручную" не полностью соответствует стандарту
ANSI C; поведение (&array[0][0])[x] не определено при x> NCOLUMNS.
gcc разрешает объявлять локальные массивы, которые имеют размеры,
задаваемые аргументами функции, но это - нестандартное расширение.
См. также вопрос 2.15.
2.12: Как объявить указатель на массив?
О: Обычно этого делать не нужно. Когда случайно говорят об указателе на
массив, обычно имеют в виду указатель на первый элемент массива.
Вместо указателя на массив рассмотрим использование указателя на один
из элементов массива. Массивы типа T превращаются в указатели типа Т
(см. вопрос 2.3), что удобно; индексация или увеличение указателя
позволяет иметь доступ к отдельным элементам массива. Истинные
указатели на массивы при увеличении или индексации указывают на
следующий массив и в общем случае если и полезны, то лишь при
операциях с массивами массивов. (Cм. вопрос 2.10 выше.)
Если действительно нужно объявить указатель на целый массив,
используйте что-то вроде "int (*ap)[N];" где N - размер массива.
(Cм. также вопрос 10.4.) Если размер массива неизвестен, параметр
N может быть опущен, но получившийся в результате тип " указатель
на массив неизвестного размера" - бесполезен.
2.13 Исходя из того, что ссылки на массив превращаются в указатели,
скажите в чем разница для массива
int array[NROWS][NCOLUMNS];
между array и &array?
O: Согласно ANSI/ISO стандарту C, &array дает указатель типа
"указатель-на-массив-Т", на весь массив (Cм. также вопрос 2.12).
В языке C до выхода стандарта ANSI оператор & в &array игнорировался,
порождая предупреждение компилятора. Все компиляторы C, встречая
просто имя массива, порождают указатель типа указатель-на-Т, т.е. на
первый элемент массива. (Cм. также вопрос 2.3.)
2.14: Как динамически выделить память для многомерного массива?
О: Лучше всего выделить память для массива указателей, а затем
инициализировать каждый указатель так, чтобы он указывал на
динамически создаваемую строку. Вот пример для двумерного массива:
int **array1 = (int **)malloc(nrows * sizeof(int *));
for(i = 0; i < nrows; i++)
array1[i] = (int *)malloc(ncolumns * sizeof(int));
(В "реальной" программе, malloc должна быть правильно объявлена, а
каждое возвращаемое malloc значение - проверено.)
Можно поддерживать монолитность массива, (одновременно затрудняя
последующий перенос в другое место памяти отдельных строк), с помощью
явно заданных арифметических действий с указателями:
int **array2 = (int **)malloc(nrows * sizeof(int *));
array2[0] = (int *)malloc(nrows * ncolumns * sizeof(int));
for(i = 1; i < nrows; i++)
array2[i] = array2[0] + i * ncolumns;
В любом случае доступ к элементам динамически задаваемого массива
может быть произведен с помощью обычной индексации: array[i][j].
Если двойная косвенная адресация, присутствующая в приведенных выше
примерах, Вас по каким-то причинам не устраивает, можно имитировать
двумерный массив с помощью динамически задаваемого одномерного
массива:
int *array3 = (int *)malloc(nrows * ncolumns * sizeof(int));
Теперь, однако, операции индексирования нужно выполнять вручную,
осуществляя доступ к элементу i,j с помощью array3[i*ncolumns+j].
(Реальные вычисления можно скрыть в макросе, однако вызов макроса
требует круглых скобок и запятых, которые не выглядят в точности
так, как индексы многомерного массива.)
Наконец, можно использовать указатели на массивы:
int (*array4)[NCOLUMNS] =
(int(*)[NCOLUMNS])malloc(nrows * sizeof(*array4));,
но синтакс становится устрашающим, и "всего лишь" одно измерение
должно быть известно во время компиляции.
Пользуясь описанными приемами, необходимо освобождать память,
занимаемую массивами (это может проходить в несколько шагов; см.
вопрос 3.9), когда они больше не нужны, и не следует смешивать
динамически создаваемые массивы с обычными, статическими (cм. вопрос
2.15 ниже, а также вопрос 2.10).
2.15 Как мне равноправно использовать статически и динамически задаваемые
многомерные массивы при передаче их в качестве параметров функциям?
О: Идеального решения не существует. Возьмем объявления
int array[NROWS][NCOLUMNS];
int **array1;
int **array2;
int *array3;
int (*array4)[NCOLUMNS];
соответствующие способам выделения памяти в вопросах 2.10 и 2.14,
и функции, объявленные как
f1(int a[][NCOLUMNS], int m, int n);
f2(int *aryp, int nrows, int ncolumns);
f3(int **pp, int m, int n);
(см. вопросы 2.10 и 2.11). Тогда следующие вызовы должны работать
так, как ожидается
f1(array, NROWS, NCOLUMNS);
f1(array4, nrows, NCOLUMNS);
f2(&array[0][0], NROWS, NCOLUMNS);
f2(*array2, nrows, ncolumns);
f2(array3, nrows, ncolumns);
f2(*array4, nrows, NCOLUMNS);
f3(array1, nrows, ncolumns);
f3(array2, nrows, ncolumns);
Следующие два вызова, возможно, будут работать, но они включают
сомнительные приведения типов, и работают лишь в том случае, когда
динамически задаваемое число столбцов ncolumns совпадает с NCOLUMS:
f1((int (*)[NCOLUMNS])(*array2), nrows, ncolumns);
f1((int (*)[NCOLUMNS])array3, nrows, ncolumns);
Необходимо еще раз отметить, что передача &array[0][0] функции f2
не совсем соответствует стандарту; см. вопрос 2.11.
Если Вы способны понять, почему все вышеперечисленные вызовы
работают и написаны именно так, а не иначе, и если Вы понимаете,
почему сочетания, не попавшие в список, работать не будут, то у Вас
_очень_ хорошее понимание массивов и указателей (и нескольких других
областей) C.
2.16 Вот изящный трюк: если я пишу
int realarray[10];
int *array = &realarray[-1];,
то теперь можно рассматривать "array" как массив, у которого индекс
первого элемента равен единице.
О: Хотя этот прием внешне привлекателен (и использовался в старых
изданиях книги "Numerical Recipes in С"), он не удовлетворяет
стандартам С. Арифметические действия над указателями определены лишь
тогда, когда указатель ссылается на выделенный блок памяти или на
воображаемый завершающий элемент, следующий сразу за блоком. В
противном случае поведение программы не определено, _даже если
указатель не переназначается_. Код, приведенный выше, плох тем,
что при уменьшении смещения может быть получен неверный адрес
(возможно, из-за циклического перехода адреса при пересечении
границы сегмента).
Смотри: ANSI Разд.3.3.6 c. 48, Rationale Разд.3.2.2.3 c. 38;
K&R II Разд.5.3 c. 100, Разд.5.4 c. 102-3, Разд.A7.7 c. 205-6.
2.17: Я передаю функции указатель, который она инициализирует
...
int *ip;
f(ip);
...
void f(ip)
int *ip;
{
static int dummy = 5;
ip = &dummy;
}
но указатель после вызова функции остается неизменным.
О: Функция пытается изменить сам указатель, или то, на что он ссылается?
Помните, что аргументы в С передаются по значению. Вызываемая функция
изменяет только копию передаваемого указателя. Вам нужно либо передать
адрес указателя (функцию будет в этом случае принимать указатель
на указатель), либо сделать так, чтобы функция возвращала указатель.
2.18 У меня определен указатель на char, который указывает еще и на int,
причем мне необходимо переходить к следующему элементу типа int.
Почему
((int *)p)++;
не работает?
О: В языке С оператор преобразования типа не означает "будем действовать
так, как будто эти биты имеют другой тип"; это оператор, который
действительно выполняет преобразования, причем по определению
получается значение типа rvalue, которому нельзя присвоить новое
значение и к которому не применим оператор ++. (Следует считать
аномалией то, что компиляторы pcc и расширения gcc вообще
воспринимают выражения приведенного выше типа.).
Скажите то, что думаете:
p = (char *)((int *)p + 1);
или просто
p += sizeof(int);
Смотри: ANSI Разд.3.3.4, Rationale Разд.3.3.2.4 c. 43.
2.19 Могу я использовать void ** , чтобы передать функции по ссылке
обобщенный указатель?
О: Стандартного решения не существует, поскольку в С нет общего типа
указатель-на-указатель. void * выступает в роли обобщенного
указателя только потому, что автоматически осуществляются
преобразования в ту и другую сторону, когда встречаются разные типы
указателей. Эти преобразования не могут быть выполнены (истинный
тип указателя неизвестен), если осуществляется попытка косвенной
адресации, когда void ** указывает на что-то отличное от void *.
3. Выделение памяти
3.1: Почему не работает фрагмент кода?
char *answer;
printf("Type something:\n");
gets(answer);
printf("You typed \"%s\"\n", answer);
О: Указатель "answer", который передается функции gets как место, в
котором должны храниться вводимые символы, не инициализирован, т.е.
не указывает на какое-то выделенное место. Иными словами, нельзя
сказать, на что указывает "answer". (Так как локальные переменные не
инициализируются, они вначале обычно содержат "мусор", то есть даже
не гарантируется, что в начале "answer" - это нулевой указатель.
См. вопрос 17.1).
Простейший способ исправить программу - использовать локальный массив
вместо указателя, предоставив компилятору заботу о выделении памяти:
#include <string.h>
char answer[100], *p;
printf("Type something:\n");
fgets(answer, sizeof(answer), stdin);
if((p = strchr(answer, '\n')) != NULL)
*p = '\0';
printf("You typed \"%s\"\n", answer);
Заметьте, что в этом примере используется fgets() вместо gets()
(это признак хорошего тона, см. вопрос 11.6), что позволяет указать
размер массива, так что выход за пределы массива, когда пользователь
введет слишком длинную строку, становится невозможным. (К сожалению,
fgets() не удаляет автоматически завершающий символ конца строки \n,
как это делает gets()). Для выделения памяти можно также
использовать malloc().
3.2: Не могу заставить работать strcat. В моей программе
char *s1 = "Hello, ";
char *s2 = "world!";
char *s3 = strcat(s1, s2);
но результаты весьма странные.
О: Проблема снова состоит в том, что не выделено место для результата
объединения. С не поддерживает автоматически переменные типа string.
Компиляторы С выделяют память только под объекты, явно указанные
в исходном тексте (в случае стрингов это может быть массив литер или
символы, заключенные в двойные кавычки). Программист должен сам
позаботиться о том, чтобы была выделена память для результата,
который получается в процессе выполнения программы, например
результата объединения строк. Обычно это достигается объявлением
массива или вызовом malloc. (см. также вопрос 17.20).
Функция strcat не выделяет память; вторая строка присоединяется к
первой. Следовательно, одно из исправлений - в задании первой строки
в виде массива достаточной длины
char s1[20] = "Hello, ";
Так как strcat возвращает указатель на первую строку (в нашем случае
s1), переменная s3 - лишняя.
Смотри: CT&P Разд. 3.2 c. 32.
3.3 Но в справочнике о функции strcat сказано, что она использует в
качестве аргументов два указателя на char. Откуда мне знать о
выделении памяти?
О: Как правило, при использовании указателей _всегда_ необходимо
иметь в виду выделение памяти, по крайней мере, быть уверенным,
что компилятор делает это для Вас. Если в документации на
библиотечную функцию явно ничего не сказано о выделении памяти,
то обычно это проблема вызывающей функции.
Краткое описание функции в верхней части страницы справочника в
стиле UNIX может ввести в заблуждение. Приведенные там фрагменты
кода ближе к определению, необходимому для разработчика функции,
чем для того, кто будет эту функцию вызывать. В частности, многие
функции, имеющие в качестве параметров указатели (на структуры или
стринги, например), обычно вызываются с параметрами, равными адресам
каких-то уже существующих объектов( структур или массивов - см.
вопросы 2.3, 2.4.) Другой распространенный пример - функция stat().
3.4 Предполагается, что функция, которую я использую, возвращает строку,
но после возврата в вызывающую функцию эта строка содержит "мусор".
О: Убедитесь, что правильно выделена область памяти, указатель на
которую возвращает Ваша функция. Функция должна возвращать указатель
на статически выделеннную область памяти или на буфер, передаваемый
функции в качестве параметра, или на память, выделенную с помощью
malloc(), но _не_ на локальный (auto) массив. Другими словами,
никогда никогда не делайте ничего похожего на
char *f()
{
char buf[10];
/* ... */
return buf;
}
Приведем одну поправку (непригодную в случае, когда f() вызывается
рекурсивно, или когда одновременно нужны несколько возвращаемых
значений)
static char buf[10];
См. также вопрос 17.5.
3.5 Почему в некоторых исходных текстах значения, возвращаемые malloc(),
аккуратно преобразуются в указатели на выделяемый тип памяти?
О: До того как стандарт ANSI/ISO ввел обобщенный тип указателя void *,
эти преобразования были обычно необходимы для подавления
предупреждений компилятора о приравнивании указателей разных типов.
(Согласно стандарту C ANSI/ISO, такие преобразования типов
указателей не требуются).
3.6 Можно использовать содержимое динамически выделяемой памяти после
того как она освобождена?
О: Нет. Иногда в старых описаниях malloc() говорилось, что содержимое
освобожденной памяти "остается неизменным"; такого рода поспешная
гарантия никогда не была универсальной и не требуется стандартом ANSI.
Немногие программисты стали бы нарочно использовать содержимое
освобожденной памяти, но это легко сделать нечаянно. Рассмотрите
следующий (корректный) фрагмент программы, в котором освобождается
память, занятая односвязным списком:
struct list *listp, *nextp;
for(listp = base; listp != NULL; listp = nextp) {
nextp = listp->next;
free((char *)listp);
}
и подумайте, что получится, если будет использовано на первый взгляд
более очевидное выражение для тела цикла listp = listp->next, без
временного указателя nextp.
См.: ANSI Rationale Разд. 4.10.3.2 c. 102; CT&P Разд. 7.10 c. 95.
3.7 Откуда free() знает, сколько байт освобождать?
О: Функции malloc/free запоминают размер каждого выделяемого и
возвращаемого блока, так что не нужно напоминать размер
освобождаемого блока.
3.8 А могу я узнать действительный размер выделяемого блока?
О: Нет универсального ответа.
3.9 Я выделяю память для структур, которые содержат указатели на
другие динамически создаваемые объекты. Когда я освобождаю память,
занятую структурой, должен ли я сначала осводить память, занятую
подчиненным объектом?
О: Да. В общем, необходимо сделать так, чтобы каждый указатель,
возвращаемый malloc() был передан free() точно один раз (если память
освобождается).
3.10 В моей программе сначала с помощью malloc() выделяется память, а
затем большое количество памяти освобождается с помощью free(), но
количество занятой памяти (так сообщает команда операционной системы)
не уменьшается.
О: Большинство реализаций malloc/free не возвращают освобожденную
память операционной системе (если таковая имеется), а просто
делают освобожденную память доступной для будущих вызовов malloc() в
рамках того же процесса.
3.11 Должен ли я освобождать выделенную память перед возвратом в
операцинную систему?
О: Делать это не обязательно. Настоящая операционная система
восстанавливает состояние памяти по окончании работы программы.
Тем не менее, о некоторых персональных компьютерах известно, что они
ненадежны при восстановлении памяти, а из стандарта ANSI/ISO
можно лишь получить указание, что эти вопросы относятся к "качеству
реализации".
См. ANSI Разд. 4.10.3.2 .
3.12 Правильно ли использовать нулевой указатель в качестве первого
аргумента функции realloc()? Зачем это нужно?
О: Это разрешено стандартом ANSI C (можно также использовать
realloc(...,0) для освобождения памяти), но некоторые ранние
реализации С это не поддерживают, и мобильность в этом случае не
гарантируется. Передача нулевого указателя realloc() может упростить
написание самостартующего алгоритма пошагового выделения памяти.
См. ANSI Разд. 4.10.3.4 .
3.13: В чем разница между calloc и malloc? Получатся ли в результате
примения calloc корректные значения нулевых указателей и чисел с
плавающей точкой? Освобождает ли free память, выделенную calloc,
или нужно использовать cfree?
О: По существу calloc(m,n) эквивалентна
p = malloc(m * n);
memset(p, 0, m * n);
Заполнение нулями означает зануление всех битов, и, следовательно,
не гарантирует нулевых значений для указателей (см. раздел 1) и
для чисел с плавающей точкой. Функция free может (и должна)
использоваться для освобождения памяти, выделенной calloc.
Смотри: ANSI Разделы от 4.10.3 до 4.10.3.2 .
3.14: Что такое alloca и почему использование этой функции обескураживает?
О: alloca выделяет память, которая автоматически освобождается, когда
происходит возврат из функции, в которой вызывалась alloca. То есть,
память, выделенная alloca, локальна по отношению к "стековому кадру"
или контексту данной функции.
Использование alloca не может быть мобильным, реализации этой
функции трудны на машинах без стека. Использование этой функции
проблематично (и очевидная реализация на машинах со стеком не
удается), когда возвращаемое ей значение непосредственно передается
другой функции, как, например, в fgets(alloca(100), 100, stdin).
По изложенным выше причинам alloca (вне зависимости от того,
насколько это может быть полезно) нельзя использовать в программах,
которые должны быть в высокой степени мобильны.
См. ANSI Rationale Разд. 4.10.3 c. 102.
4. Выражения
4.1: Почему вот такой код
a[i] = i++;
не работает?
О: Подвыражение i++ приводит к побочному эффекту - значение i
изменяется, что приводит к неопределенности, если i уже встречается в
том же выражении. (Обратите внимание на то, что хотя в книге K&R
говорится, что поведение подобных выражений не описано, стандарт
ANSI/ISO утверждает, что поведение не определено - см. вопрос 5.23.)
См. ANSI Разд.3.3 c. 39.
4.2: Пропустив код
int i = 7;
printf("%d\n", i++ * i++);
через свой компилятор, я получил на выходе 49. А разве, независимо
от порядка вычислений, результат не должен быть равен 56?
О: Хотя при использовании постфиксной формы операторов ++ и --
увеличение и уменьшение выполняется после того как первоначальное
значение использовано, тайный смысл слова "после" часто понимается
неверно. _Не_ гарантируется, что увеличение или уменьшение будет
выполнено немедленно после использования первоначального значения
перед тем как будет вычислена любая другая часть выражения. Просто
гарантируется, что измение будет произведено в какой-то момент до
окончания вычисления (перед следующей "точкой последовательности"
в терминах ANSI C). В приведенном примере компилятор умножил
предыдущее значение само на себя и затем дважды увеличил i на 1.
Поведение кода, содержащего многочисленные двусмысленные побочные
эффекты неопределено (см. вопрос 5.23). Даже не пытайтесь выяснить,
как Ваш компилятор все это делает (в противоположность неумным
упражнениям во многих книгах по С); в K&R мудро сказано: "Да
хранит Вас Ваша невинность, если Вы не знаете, как это делается
на разных машинах"
См.: K&R I Разд. 2.12 c. 50; K&R II Разд. 2.12 c. 54; ANSI
Разд. 3.3 c. 39; CT&P Разд. 3.7 c. 47; PCS Разд. 9.5 c. 120-1.
(Не принимайте во внимание H&S Разд. 7.12 c. 190-1, это устарело)
4.3: Я экспериментировал с кодом
int i = 2;
i = i++;
Некоторые компиляторы выдавали i=2, некоторые 3, но один выдал 4.
Я знаю, что поведение неопределено, но как можно получить 4?
О: Неопределенное (undefined) поведение означает, что может случиться
_все_ что угодно. См. вопрос 5.23.
4.4 Люди твердят, что поведение неопределено, а я попробовал ANSI -
компилятор и получил то, что ожидал.
О: Компилятор делает все, что ему заблагорассудится, когда встречается
с неопределенным поведением (до некоторой степени это относится и к
случаю зависимого от реализации и неописанного поведения). В
частности, он может делать то, что Вы ожидаете. Неблагоразумно,
однако, полагаться на это. См. также вопрос 5.18.
4.5: Могу я использовать круглые скобки, чтобы обеспечить нужный мне
порядок вычислений? Если нет, то разве приоритет операторов не
обеспечивает этого?
О: Круглые скобки, как и приоритет операторов обеспечивают лишь
частичный порядок при вычислении выражений. Рассмотрим выражение
f() + g() * h() .
Хотя известно, что умножение будет выполнено раньше сложения, нельзя
ничего сказать о том, какая из трех функций будет вызвана первой.
4.6 Тогда как насчет операторов &&, ||, и запятой ? Я имею в виду код
типа
if((c = getchar()) == EOF || c == '\n')" ...
О: Для этих операторов, как и для оператора ?: существует специальное
исключение; каждый из них подразумевает определенный порядок
вычислений, т.е. гарантируется вычисление слева-направо. В любой
книге по С эти вопросы должны быть ясно изложены.
См.: K&R I Разд. 2.6 c. 38, Разд. A7.11-12 c. 190-1;
K&R II Разд. 2.6 c. 41, Разд. A7.14-15 c. 207-8; ANSI
Разд. 3.3.13 c. 52, 3.3.14 c. 52, 3.3.15 c. 53, 3.3.17 c. 55,
CT&P Разд. 3.7 c. 46-7.
4.7 Если я не использую значение выражения, то как я должен увеличивать
переменную i: так: ++i или так: i++ ?
О: Применение той или иной формы сказывается только на значении
выражения, обе формы полностью эквивалентны, когда требуются только
их побочные эффекты.
4.8 Почему неправильно работает код
int a = 1000, b = 1000;
long int c = a * b; ?
О: Согласно общим правилам преобразования типов языка С, умножение
выполняется с использованием целочисленной арифметики, и результат
может привести к переполнению и/или усечен до того как будет присвоен
стоящей слева переменной типа long int. Используйте явное приведение
типов, чтобы включить арифметику длинных целых
long int c = (long int)a * b;
Заметьте, что код (long int)(a * b) _не_ приведет к желаемому
результату.
5. ANSI C
5.1 Что такое стандарт ANSI C ?
О: В 1983 году Американский институт национальных стандартов (ANSI)
учредил комитет X3J11, чтобы разработать стандарт языка С. После
длительной и трудной работы, включающей выпуск нескольких публичных
отчетов, работа комитета завершилась 14 декабря 1989 г.созданием
стандарта ANS X3.159-1989. Стандарт был опубликован весной 1990 г.
В большинстве случаев ANSI C узаконил уже существующую практику и
сделал несколько заимствований из С++ (наиболее важное - введение
прототипов функций). Была также добавлена поддержка национальных
наборов символов (включая подвергшиеся наибольшим нападкам
трехзнаковые последовательности). Стандарт ANSI C формализовал также
стандартную библиотеку.
Опубликованный стандарт включает "Комментарии" ("Rationale"), в
которых объясняются многие решения и обсуждаются многие тонкие
вопросы, включая несколько затронутых здесь. ("Комментарии"
не входят в стандарт ANSI X3.159-1989, они приводятся в качестве
дополнительной информации.)
Стандарт ANSI был принят в качестве международного стандарта ISO/IEC
9899:1990, хотя нумерация разделов иная (разделы 2 - 4 стандарта
ANSI соответствуют разделам 5 - 7 стандарта ISO), раздел
"Комментарии" не был включен.
5.2 Как получить копию Стандарта?
ANSI X3.159 был официально заменен стандартом ISO 9899. Копию
стандарта можно получить по адресу
American National Standards Institute
11 W. 42nd St., 13th floor
New York, NY 10036 USA
(+1) 212 642 4900
или
Global Engineering Documents
2805 McGaw Avenue
Irvine, CA 92714 USA
(+1) 714 261 1455
(800) 854 7179 (U.S. & Canada)
В других странах свяжитесь с местным комитетом по стандартам или
обратитесь в Национальный Комитет по Стандартам в Женеве
ISO Sales
Case Postale 56
CH-1211 Geneve 20
Switzerland
Цена составляет в ANSI $130, в Global Engineering Documents - $160.
Копии оригинального стандарта Х3.159, включающие "Комментарии",
попрежнему доступны за $205.00 (ANSI) или за $200.50 (Global
Engineering Documents). Отметим, что комитет ANSI для поддержки своей
деятельности получает доход от продажи отпечатанных копий стандарта,
так что электронные копии _недоступны_.
Книга Герберта Шилдта с обманчивым названием "Комментарии к стандарту
С" содержит лишь несколько страниц стандарта ISO 9899; опубликована
издательством Osborne/McGraw-Hill, ISBN 0-07-881952-0 и продается
примерно за $40. (Есть мнение, что различие в цене между официальным
стандартом и комментарием Герберта Шилдта соответствует ценности
комментария).
Текст "Комментариев" (не всего стандарта) теперь доступен через
ftp ftp.uu.net (см. вопрос 17.12) в директории doc/standards/ansi/
X3.159-1989. "Комментарии" были также изданы издательством Silicon
Press, ISBN 0-929306-07-4.
5.3 Есть ли у кого-нибудь утилиты для перевода С-программ, написанных в
в старом стиле, в ANSI C и наоборот? Существуют ли программы для
автоматического создания прототипов?
О: Две программы, protoize и unprotoize осуществляют преобразование в
обе стороны между функциями, записанными в новом стиле с прототипами,
и функциями, записанными в старом стиле. (Эти программы не
поддерживают полный перевод между "классическим" и ANSI C).
Упомянутые программы были сначала вставками в FSF GNU компилятор С,
gcc, но теперь они - часть дистрибутива gcc; смотри директорий
pub/gnu на prep.ai.mit.edu (18.71.0.38), или в других архивах FSF.
Программа unproto ((/pub/unix/unproto5.shar.Z на ftp.win.tue.nl -
это фильтр, располагающийся между препроцессором и следующим
проходом компилятора - на лету переводит большинство особенностей
ANSI C в традиционный С.
GNU пакет GhostScript содержит маленькую программу ansi2knr.
Есть несколько генераторов прототипов, многие из них - модификации
программы lint. Версия 3 программы CPROTO была помещена в конференцию
comp.sources.misc в марте 1992 г. Есть другая программа, которая
называется "ctxtract". См. вопрос 17.12.
В заключение хочется спросить: так ли уж нужно преобразовывать
огромное количество старых программ в ANSI C? Старый стиль написания
функций все еще допустим.
5.4 Я пытаюсь использовать ANSI- строкообразующий оператор #, чтобы
вставить в сообщение значение символической константы, но вставляется
формальный параметр макроса, а не его значение.
О: Необходимо использовать двухшаговую процедуру для того чтобы макрос
раскрывался как при строкообразовании
#define str(x) #x
#define xstr(x) str(x)
#define OP plus
char *opname = xstr(OP);
Такая процедура устанавливает opname равным "plus", а не "OP".
Такие же обходные маневры необходимы при использовании оператора
склеивания лексем ##, когда нужно соединить значения (а не имена
формальных параметров) двух макросов.
Смотри: ANSI Разд. 3.8.3.2, Разд. 3.8.3.5 пример c. 93.
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 .
9.3 Каков механизм передачи и возврата структур?
О: Структура, передаваемая функции как параметр, обычно целиком
размещается на стеке, используя необходимое количество машинных слов.
(Часто для снижения ненужных затрат программисты предпочитают
передавать функции указатель на структуру вместо самой структуры).
Структуры часто возвращаются функциями в ту область памяти, на
которую указывает дополнительный поддерживаемый компилятором
"скрытый" аргумент. Некоторые старые компиляторы используют для
возврата структур фиксированную область памяти, хотя это делает
невозможным рекурсивный вызов такой функции, что противоречит
стандарту ANSI.
Смотри: ANSI Разд.2.2.3 c. 13.
9.4 Эта программа работает правильно, но после завершения выдает дамп
оперативной памяти. Почему?
struct list
{
char *item;
struct list *next;
}
/* Здесь функция main */
main(argc, argv)
...
О: Из-за пропущенной точки с запятой компилятор считает, что main
возвращает структуру. (Связь структуры с функцией main трудно
определить, мешает комментарий). Так как для возврата структур
компилятор обычно использует в качестве скрытого параметра
указатель, код, сгенерированный для main() пытается принять три
аргумента, хотя передаются (в данном случае стартовым кодом С)
только два. См. также вопрос 17.21.
9.5 Почему нельзя сравнивать структуры?
О: Не существует разумного способа сделать сравнение структур
совместимым с низкоуровневой природой языка С. Побайтовое сравнение
может быть неверным из-за случайных бит в неиспользуемых "дырках"
(такое заполнение необходимо, чтобы сохранить выравнивание для
последующих полей; см. вопросы 9.10 и 9.11). Почленное сравнение
потребовало бы неприемлевого количества повторяющихся машинных
инструкций в случае больших структур.
Если необходимо сравнить две структуры, напишите для этого свою
собственную функцию. C++ позволит создать оператор ==, чтобы
связать его с Вашей функцией.
Смотри: K&R II Разд.6.2 c. 129; H&S Разд. 5.6.2 c. 103; ANSI
Rationale разд. 3.3.9 c. 47.
9.7 Как читать/писать структуры из файла/в файл ?
О: Писать структуры в файл можно непосредственно с помощью fwrite:
fwrite((char *)&somestruct, sizeof(somestruct), 1, fp);
a cоответствующий вызов fread прочитает структуру из файла.
Однако файлы, записанные таким образом будут _не_ особенно переносимы
(см. вопросы 9.11 и 17.3). Заметьте также, что на многих системах
нужно использовать в функции fopen флаг "b" .
9.7 Мне попалась программа, в которой структура определяется так:
struct name
{
int namelen;
char name[1];
};
затем идут хитрые манипуляции с памятью, чтобы массив name вел себя
будто в нем несколько элементов. Такие манипуляции законны/мобильны?
О: Такой прием популярен, хотя Деннис Ритчи назвал это "слишком
фамильярным обращением с реализацией С". ANSI полагает, что выход за
пределы объявленного размера члена структуры не полностью соответствует
стандарту, хотя детальное обсуждение всех связанных с этим проблем
не входит в задачу данных вопросов и ответов. Похоже, однако, что
описанный прием будет одинаково хорошо принят всеми известными
реализациями С. (Компиляторы, тщательно проверяющие границы массивов,
могут выдать предупреждения). Для страховки будет лучше объявить
переменную очень большого размера чем очень малого. В нашем случае
...
char name[MAXSIZE];
...
где MAXSIZE больше, чем длина любого имени, которое будет сохранено
в массиве name[]. (Есть мнение, что такая модификация будет
соответствовать Стандарту).
Смотри: ANSI Rationale Разд. 3.5.4.2 c. 54-5.
9.8 Как определить смещение члена структуры в байтах?
О: Если это возможно, необходимо использовать макрос offsetof, который
определен стандартом ANSI; см. <stddef.h>. Если макрос отсутствует,
предлагается такая (не на все 100% мобильная) его реализация
#define offsetof(type, mem) ((size_t) \
((char *)&((type *) 0)->mem - (char *)((type *) 0)))
Для некоторых компиляторов использование этого макроса может оказаться
незаконным.
О том, как использовать offsetof(), смотри следующий вопрос.
Смотри: ANSI Разд. 4.1.5, Rationale Разд. 3.5.4.2 c. 55.
9.9 Как осуществить доступ к членам структур по их именам во время
выполнения программы?
О: Создайте таблицу имен и смещений, используя макрос offsetof().
Смещение члена структуры b в структуре типа a равно
offsetb = offsetof(struct a, b)
Если structp указывает на начало структуры, а b - член структуры типа
int, смещение которого получено выше, b может быть установлен
косвенно с помощью
*(int *)((char *)structp + offsetb) = value;
9.10 Почему sizeof выдает больший размер структурного типа, чем я ожидал,
как будто в конце структры лишние символы?
О: Это происходит (возможны также внутренние "дыры" ; см.
также вопрос 9.5), когда необходимо выравнивание при задании массива
непрерывных структур.
9.11 Мой компилятор оставляет дыры в структурах, что приводит к потере
памяти и препятствует "двоичному" вводу/выводу при работе с
внешними файлами. Могу я отключить "дырообразование" или как-то
контролировать выравнивание?
О: В Вашем компиляторе, возможно, есть расширение, (например,
#pragma), которое позволит это сделать, но стандартного способа не
существует. См. также вопрос 17.3.
9.12 Можно ли задавать начальные значения объединений?
О: Стандарт ANSI допускает инициализацию первого члена объединения.
Не существует стандартного способа инициализации других членов.
(и тем более нет такого способа для старых компиляторов, которые
вообще не поддерживают какой-либо инициализации).
9.13 Как передать функциии структуру, у которой все члены - константы?
О: Поскольку в языке С нет возможности создавать безымянные значения
структурного типа, необходимо создать временную структуру.
10. Декларации
10.1 Какой тип целочисленной переменной использовать?
О: Если могут потребоваться большие числа, (больше 32767 или меньше
-32767), используйте тип long. Если нет, и важна экономия памяти
(большие массивы или много структур), используйте short. Во всех
остальных случаях используйте int. Если важно точно определить
момент переполнения и/или знак числа не имеет значения, используйте
соответствующий тип unsigned. (Но будьте внимательны при совместном
использовании типов signed и unsigned в выражениях). Похожие
соображения применимы при выборе между float и double.
Хотя тип char или unsigned char может использоваться как
целочисленный тип наименьшего размера, от этого больше вреда,
чем пользы из-за непредсказуемых перемен знака и возрастающего
размера программы.
Эти правила, очевидно, не применимы к адресам переменных, поскольку
адрес должен иметь совершенно определенный тип.
Если необходимо объявить переменную _определенного_ размера,
(единственной причиной тут может быть попытка удовлетворить внешним
требованиям к организации памяти; см.,кроме того, вопрос 17.3),
непременно изолируйте объявление соответствующим typedef.
10.2 Каким должен быть новый 64-битный тип на новых 64-битных машинах?
О: Некоторые поставщики С компиляторов для 64-битных машин поддерживают
тип long int длиной 64 бита. Другие же, опасаясь, что слишком многие
уже написанные программы зависят от sizeof(int) == sizeof(long) == 32
бита, вводят новый 64-битный тип long long (или __longlong).
Программисты, желающие писать мобильные программы, должны,
следовательно, изолировать 64-битные типы с помощью средства typedef.
Разработчики компиляторов, чувствующие необходимость ввести новый
целочисленный тип большего размера, должны объявить его как "имеющий
по крайней мере 64 бит" (это действительно новый тип, которого нет
в традиционном С), а не как "имеющий точно 64 бит".
10.3 У меня совсем не получается определение связанного списка. Я пишу
typedef struct
{
char *item;
NODEPTR next;
} *NODEPTR;
но компилятор выдает сообщение об ошибке. Может структура в С
содержать ссылку на себя?
О: Структуры в С, конечно же, могут содержать указатели на себя;
обсуждение этого вопроса и пример в параграфе 6.5 K&R вполне
проясняют этот вопрос. В приведенном тексте проблема состоит в том,
что определение NODEPTR не закончено в том месте, где объявлется
член структуры "next". Для исправления, снабдите сначала структуру
тегом ("struct node"). Далее объявите "next" как "struct node
*next;", и/или поместите декларацию typedef целиком до или целиком
после объявления структуры. Одно из возможных решений будет таким:
struct node
{
char *item;
struct node *next;
};
typedef struct node *NODEPTR;
Есть по крайней мере три других одинаково правильных способа
сделать то же самое.
Сходная проблема, которая решается примерно так же, может возникнуть
при попытке определить с помощью средства typedef пару cсылающихся
друг на друга структур.
Смотри: K&R I Разд. 6.5 c. 101; K&R II Разд. 6.5 c. 139; H&S
Разд. 5.6.1 c. 102; ANSI Разд. 3.5.2.3 .
10.4 Как объявить массив из N указателей на функции, возвращающие
указатели на функции возвращающие указатели на char?
О: Есть по крайней мере три варианта ответа:
1. char *(*(*a[N])())();
2. Писать декларации по шагам, используя typedef:
typedef char *pc; /* указатель на char */
typedef pc fpc(); /* функция,возвращающая указатель на char */
typedef fpc *pfpc; /* указатель на.. см. выше */
typedef pfpc fpfpc(); /* функция, возвращающая... */
typedef fpfpc *pfpfpc; /* указатель на... */
pfpfpc a[N]; /* массив... */
3. Использовать программу cdecl, которая переводит с английского
на С и наоборот.
cdecl> declare a as array of pointer to function returning
pointer to function returning pointer to char
char *(*(*a[])())()
cdecl может также объяснить сложные декларации, помочь при явном
приведении типов, и, для случая сложных деклараций, вроде только что
разобранного, показать набор круглых скобок, в которые заключены
аргументы. Версии cdecl можно найти в comp.sources.unix (см. вопрос
17.12) и в K&R II. Любая хорошая книга по С должна объяснять, как
для понимания сложных деклараций, читать их "изнутри наружу",
("декларация напоминает использование").
Смотри: K&R II Разд. 5.12 c. 122; H&S Разд. 5.10.1 c. 116.
10.5 Я моделирую Марковский процесс с конечным числом состояний, и у меня
есть набор функций для каждого состояния. Я хочу, чтобы смена
состояний происходила путем возврата функцией указателя на функцию,
соответветствующую следующему состоянию. Однако, я обнаружил
ограничение в механизме деклараций языка С: нет возможности объявить
функцию, возвращающую указатель на функцию, возвращающую указатель
на функцию, возвращающую указатель на функцию...
О: Да, непосредственно это сделать нельзя. Пусть функция возвращает
обобщенный указатель на функцию, к которому перед вызовом функции
будет применен оператор приведения типа, или пусть она возвращает
структуру, содержащую только указатель на функцию, возвращающую
эту структуру.
10.6 Мой компилятор выдает сообщение о неверной повторной декларации,
хотя я только раз определил функцию и только раз вызвал.
О: Подразумевается, что функции, вызываемые без декларации в области
видимости (или до такой декларации), возвращают значение типа int.
Это приведет к противоречию, если впоследствии функция декларирована
иначе. Если функция возвращает нецелое значение, она должна быть
объявлена до того как будет вызвана.
Смотри: K&R I Разд. 4.2 c. 70; K&R II Разд. 4.2 c. 72; ANSI
Разд. 3.3.2.2 .
10.7 Как наилучшим образом декларировать и определить глобальные
переменные?
О: Прежде всего заметим, что хотя может быть много _деклараций_ (и во
многих файлах) одной "глобальной" (строго говоря "внешней" )
переменной, (или функции), должно быть всего одно _определение_.
(Определение - это такая декларация, при которой действительно
выделяется память для переменной, и присваивается, если нужно,
начальное значение). Лучше всего поместить определение в какой-то
главный (для программы или ее части) .c файл, с внешней декларацией в
головном файле .h, который при необходимости подключается с помощью
#include. Файл, в котором находится определение переменной, также
должен включать головной файл с внешней декларацией, чтобы компилятор
мог проверить соответствие декларации и определения.
Это правило обеспечивает высокую мобильность программ и находится в
согласии с требованиями стандарта ANSI C. Заметьте, что многие
компиляторы и компоновщики в системе UNIX используют "общую модель",
которая разрешает многократные определения без инициализации.
Некоторые весьма странные компиляторы могут требовать явной
инициализации, чтобы отличить определение от внешней декларации.
С помощью препроцессорного трюка можно устроить так, что декларация
будет сделана лишь однажды, в головном файле, и она c помощью #define
"превратится" в определение точно при одном включении головного
файла.
Смотри: K&R I Разд. 4.5 c. 76-7; K&R II Разд. 4.4 c. 80-1;
ANSI Разд. 3.1.2.2 (особенно Rationale), Разд. 3.7, 3.7.2,
Разд. F.5.11; H&S Разд. 4.8 c. 79-80; CT&P Разд. 4.2 c. 54-56.
10.8 Что означает ключевое слово extern при декларации функции?
О: слово extern при декларации функции может быть использовано из
соображений хорошего стиля для указания на то, что определение
функции, возможно, находится в другом файле. Формально между
extern int f();
и
int f();
нет никакой разницы.
Смотри: ANSI Разд. 3.1.2.2 .
10.9 Я, наконец, понял, как объвлять указатели на функции, но как их
инициализировать?
О: Используйте нечто такое
extern int func();
int (*fp)() = func;
Когда имя функции появляется в выражении, но функция не вызывается
(то есть, за именем функции не следует "(" ), оно "сворачивается",
как и в случае массивов, в указатель (т.е. неявным образом записанный
адрес).
Явное объявление функции обычно необходимо, так как неявного
объявления внешней функции в данном случае не происходит (опять-таки
из-за того, что за именем функции не следует "(" ).
10.10 Я видел, что функции вызываются с помощью указателей и просто как
функции. В чем дело?
О: По первоначальному замыслу создателя С указатель на функцию должен
был "превратиться" в настоящую функцию с помощью оператора *
и дополнительной пары круглых скобок для правильной интерпретации.
int r, func(), (*fp)() = func;
r = (*fp)();
На это можно возразить, что функции всегда вызываются с помощью
указателей, но что "настоящие" функции неявно превращаются в
указатели (в выражениях, как это происходит при инициализациях) и это
не приводит к каким-то проблемам. Этот довод, широко распространенный
компилятором pcc и принятый стандартом ANSI, означает, что выражение
r = fp();
работает одинаково правильно, независимо от того, что такое fp -
функция или указатель на нее. (Имя всегда используется однозначно;
просто невозможно сделать что-то другое с указателем на функцию,
за которым следует список аргументов, кроме как вызвать функцию).
Явное задание * безопасно и все еще разрешено (и рекомендуется,
если важна совместимость со старыми компиляторами).
Смотри: ANSI Разд. 3.3.2.2 c. 41, Rationale c. 41.
10.11 Где может пригодиться ключевое слово auto?
О: Нигде, оно вышло из употребления.
11. Cтандартный ввод/вывод.
11.1 Что плохого в таких строках:
char c;
while((c = getchar()) != EOF)...
О: Во-первых, переменная, которой присваивается возвращенное getchar
значение, должна иметь тип int. getchar может вернуть все возможные
значения для символов, в том числе EOF. Если значение, возвращенное
getchar присваивается переменной типа char, возможно либо обычную
литеру принять за EOF, либо EOF исказится (особенно если использовать
тип unsigned char) так, что распознать его будет невозможно.
Смотри: CT&P Разд.5.1 c. 70.
11.2 Как напечатать символ '%' в строке формата printf? Я попробовал
\%, но из этого ничего не вышло.
О: Просто удвойте знак процента %% .
Смотри: K&R I Разд. 7.3 c. 147; K&R II Разд. 7.2 c. 154; ANSI
Разд. 4.9.6.1 .
11.3 Почему не работает scanf("%d",i)?
О: Для функции scanf необходимы адреса переменных, по которым будут
записаны данные, нужно написать scanf("%d", &i);
11.4 Почему не работает
double d;
scanf("%f", &d);
О: scanf использует спецификацию формата %lf для значений типа double
и %f для значений типа float. (Обратите внимание на несходство с
printf, где в соответствии с правилом расширения типов аргументов
спецификация %f используется как для float, так и для double).
11.5 Почему фрагмент программы
while(!feof(infp)) {
fgets(buf, MAXLINE, infp);
fputs(buf, outfp);
}
дважды копирует последнюю строку?
О: Это Вам не Паскаль. Символ EOF появляется только _после_ попытки
прочесть, когда функция ввода натыкается на конец файла.
Чаще всего необходимо просто проверять значение, возвращаемое
функцией ввода, (в нашем случае fgets); в использовании feof()
обычно вообще нет необходимости.
11.6 Почему все против использования gets()?
О: Потому что нет возможности предотвратить переполнение буфера,
куда читаются данные, ведь функции gets() нельзя сообщить его
размер. Смотрите вопрос 3.1, в котором приведен фрагмент программы,
показывающий замену gets() на fgets().
11.7 Почему переменной errno присваивается значение ENOTTY после вызова
printf()?
О: Многие реализации стандартной библиотеки ввода/вывода несколько
изменяют свое поведение, если стандартное устройство вывода -
терминал. Чтобы определить тип устройства, выполняется операция,
которая оканчивается неудачно (c сообщением ENOTTY), если устройство
вывода - не терминал. Хотя вывод завершается успешно, errno все же
содержит ENOTTY.
Смотри: CT&P Разд. 5.4 c. 73.
11.8 Запросы моей программы, а также промежуточные результаты не всегда
отображаются на экране, особенно когда моя программа передает данные
по каналу (pipe) другой программе.
О: Лучше всего явно использовать fflush(stdout), когда непременно
нужно видеть то, что выдает программа. Несколько механизмов пытаются
"в нужное время" осуществить fflush, но, похоже, все это правильно
работает в том случае, когда stdout - это терминал. (см. вопрос
11.7).
11.9 При чтении с клавиатуры функцией scanf возникает чувство, что
программа зависает, пока я перевожу строку.
О: Функция scanf была задумана для ввода в свободном формате,
необходимость в котором возникает редко при чтении с клавиатуры.
Что же касается ответа на вопрос, то символ "\n" в форматной строке
вовсе не означает, что scanf будет ждать перевода строки. Это
значит, что scanf будет читать и отбрасывать все встретившиеся подряд
пробельные литеры (т.е. символы пробела, табуляции, новой строки,
возврата каретки, вертикальной табуляции и новой страницы).
Похожее затруднение случается, когда scanf "застревает", получив
неожиданно для себя нечисловые данные. Из-за подобных проблем часто
лучше читать всю строку с помощью fgets, а затем использовать sscanf
или другие функции, работающие со строками, чтобы интерпретировать
введенную строку по частям. Если используется sscanf, не забудьте
проверить возвращаемое значение для уверенности в том, что число
прочитанных переменных равно ожидаемому.
11.10 Я пытаюсь обновить содержимое файла, для чего использую fopen в
режиме "r+", далее читаю строку, затем пишу модифицированную строку
в файл, но у меня ничего не получается.
О: Непременно вызовите fseek перед записью в файл. Это делается
для возврата к началу строки, которую Вы хотите переписать; кроме
того, всегда необходимо вызвать fseek или fflush между чтением и
записью при чтении/записи в режимах "+". Помните также, что литеры
можно заменить лишь точно таким же числом литер. См. также
вопрос 17.4.
Смотри: ANSI Разд. 4.9.5.3 c. 131.
11.11 Как мне прочитать одну литеру, не дожидаясь нажатия RETURN?
О: Смотри вопрос 16.1
11.12 Как мне отменить ожидаемый ввод, так, чтобы данные, введенные
пользователем, не читались при следующем запросе? Поможет ли здесь
fflush(stdin)?
О: fflush определена только для вывода. Поскольку определение "flush"
("смывать") означает завершение записи символов из буфера (а не
отбрасывание их), непрочитанные при вводе символы не будут уничтожены
с помощью fflush. Не существует стандартного способа игнорировать
символы, еще не прочитанные из входного буфера stdio. Не видно также,
как это вообще можно сделать, поскольку непрочитанные символы могут
накапливаться в других, зависящих от операциооной системы, буферах.
11.13 Как перенаправить stdin или stdout в файл?
О: Используйте freopen.
11.14 Если я использовал freopen, то как вернуться назад к stdout (stdin)?
О: Если необходимо переключаться между stdin (stdout) и файлом,
наилучшее универсальное решение - не спешить использовать freopen.
Попробуйте использовать указатель на файл, которому можно по желанию
присвоить то или иное значение, оставляя значение stdout (stdin)
нетронутым.
11.15 Как восстановить имя файла по указателю на открытый файл?
О: Это проблема, вообще говоря, неразрешима. В случае операционной
системы UNIX, например, потребуется поиск по всему диску (который,
возможно, потребует специального разрешения), и этот поиск окончится
неудачно, если указатель на файл был каналом (pipe) или был связан
с удаленным файлом. Кроме того, обманчивый ответ будет получен для
файла со множественными связями. Лучше всего самому запоминать имена
при открытии файлов (возможно, используя специальные функции,
вызываемые до и после fopen);
12. Библиотечные функции.
12.1 Почему strncpy не всегда завершает строку-результат символом '\0'?
О: strncpy была задумана для обработки теперь уже устаревших структур
данных - "строк" фиксированной длины, не обязательно завершающихся
символом '\0'. И, надо сказать, strncpy не совсем удобно
использовать в других случаях, поскольку часто придется добавлять
символ '\0' вручную.
12.2 Я пытаюсь сортировать массив строк с помощью qsort, используя
для сравнения strcmp, но у меня ничего не получается.
О: Когда Вы говорите о "массиве строк", то, видимо, имеете в виду
"массив указателей на char". Аргументы функции сравнения, работающей
в паре с qsort - это указатели на сравниваемые объекты, в данном
случае - указатели на указатели на char. (Конечно, strcmp работает
просто с указателями на char).
Аргументы процедуры сравнения описаны как "обобщенные указатели"
const void * или char *. Они должны быть превращены в то, что они
представляют на самом деле, т.е. (char **) и дальше нужно раскрыть
ссылку с помощью * ; тогда strcmp получит именно то, что нужно для
сравнения. Напишите функцию сравнения примерно так:
int pstrcmp(p1, p2) /* сравнить строки, используя указатели */
char *p1, *p2; /* const void * для ANSI C */
{
return strcmp(*(char **)p1, *(char **)p2);
}
Имейте в виду, что в K&R II Разд. 5.11 обсуждается функция qsort,
которая отличается от стандартной.
12.3 Сейчас я пытаюсь сортировать массив структур с помощью qsort.
Процедура сравнения, которую я использую, принимает в качестве
аргументов указатели на структуры, но компилятор выдает сообщение
о неверном типе функции сравнения. Как мне преобразовать аргументы
функции, чтобы подавить сообщения об ошибке?
О: Преобразования должны быть сделаны внутри функци сравнения, которая
должна быть объявлена как принимающая аргументы типа "обобщенных
указателей (const void * или char *) как это описано в вопросе 12.2.
Функция сравнения может выглядеть так:
int mystructcmp(p1, p2)
char *p1, *p2; /* const void * для ANSI C */
{
struct mystruct *sp1 = (struct mystruct *)p1;
struct mystruct *sp2 = (struct mystruct *)p2;
/* теперь сравнивайте sp1->что-угодно и sp2-> ... */
}
(С другой стороны, если сортируются указатели на структуры,
необходима косвенная адресация, как в вопросе 12.2:
sp1 = *(struct mystruct **)p1 .)
12.4 Как преобразовать числа в строки (операция, противоположная atoi)?
Есть ли функция itoa?
О: Просто используйте sprintf. (Необходимо будет выделить память для
результата, см. вопросы 3.1 и 3.2. Беспокоиться, что sprintf -
слишком сильное средство, которое может привести к перерасходу
памяти и увеличению времени выполнения, нет оснований. На практике
sprintf работает хорошо).
Смотри: K&R I Разд.3.6 c. 60; K&R II Разд.3.6 c. 64.
12.5 Как получить дату или время в С программе?
О: Просто используйте функции time, ctime, и/или localtime. (Эти
функции существуют многие годы, они включены в стандарт ANSI).
Вот простой пример:
#include <stdio.h>
#include <time.h>
main()
{
time_t now = time((time_t *)NULL);
printf("It's %.24s.\n", ctime(&now));
return 0;
}
Смотри: ANSI Разд. 4.12 .
12.6 Я знаю, что библиотечная функция localtime разбивает значение time_t
по отдельным членам структуры tm, а функция ctime превращает time_t в
строку символов. А как проделать обратную операцию перевода
структуры tm или строки символов в значение time_t?
О: Стандарт ANSI определяет библиотеную функцию mktime, которая
преобразует структуру tm в time_t. Если Ваш компилятор не
поддерживает mktime, воспользуйтесь одной из общедоступных версий
этой функции.
Перевод строки в значение time_t выполнить сложнее из-за большого
количества форматов дат и времени, которые должны быть распознаны.
Некоторые компиляторы поддерживают функцию strptime; другая
популярная функция - partime широко распространяется с пакетом RCS,
но нет уверенности, что эти функции войдут в Стандарт.
Смотри: K&R II Разд. B10 c. 256; H&S Разд. 20.4 c. 361; ANSI
Разд. 4.12.2.3 .
12.7 Как прибавить n дней к дате? Как вычислить разность двух дат?
О: Вошедшие в стандарт ANSI/ISO функции mktime и difftime могут помочь
при решении обеих проблем. mktime() поддерживает ненормализованные
даты, т.е. можно прямо взять заполненную структуру tm, увеличить или
уменьшить член tm_mday, затем вызвать mktime(), чтобы нормализовать
члены year, month, и day (и преобразовать в значение time_t).
difftime() вычисляет разность в секундах между двумя величинами
типа time_t. mktime() можно использовать для вычисления значения
time_t разности двух дат. (Заметьте, однако, что все эти приемы
возможны лишь для дат, которые могут быть представлены значением
типа time_t; кроме того, из-за переходов на летнее и зимнее время
продолжительность дня не точно равна 86400 сек.).
Cм. также вопросы 12.6 и 17.28.
Смотри: K&R II Разд. B10 c. 256; H&S Разд. 20.4, 20.5
c. 361-362; ANSI Разд. 4.12.2.2, 4.12.2.3 .
12.8 Мне нужен генератор случайных чисел.
О: В стандартной библиотеке С есть функция rand(). Реализация этой
функции в Вашем компиляторе может не быть идеальной, но и создание
лучшей функции может оказаться очень непростым.
Смотри: ANSI Разд. 4.10.2.1 c. 154; Knuth Vol. 2 Chap. 3
c. 1-177.
12.9 Как получить случайные целые числа в определенном диапазоне?
О: Очевидный способ
rand() % N
где N, конечно, интервал, довольно плох, ведь поведение младших
бит во многих генераторах случайных чисел огорчает своей
неслучайностью. (См. вопрос 12.11). Лучше попробуйте нечто вроде
(int)((double)rand() / ((double)RAND_MAX + 1) * N)
Если Вам не нравится употребление чисел с плавающей точкой,
попробуйте
rand() / (RAND_MAX / N + 1)
Оба метода требуют знания RAND_MAX (согласно ANSI, RAND_MAX определен
в <stdlib.h>. Предполагается, что N много меньше RAND_MAX.
12.10 Каждый раз при запуске программы функция rand() выдает одну и ту же
последовательность чисел.
О: Можно вызвать srand() для случайной инициализации генератора
случайных чисел. В качестве аргумента для srand() часто используется
текущее время, или время, прошедшее до нажатия на клавишу (хотя едва
ли существует мобильная процедура определения времен нажатия на
клавиши; см. вопрос 16.10).
Смотри: ANSI Разд. 4.10.2.2 c. 154.
12.11 Мне необходима случайная величина, имеющая два значения true/false.
Я использую rand() % 2, но получается неслучайная последовательность
0,1,0,1,0....
О: Некачественные генераторы случайных чисел (попавшие, к несчастью, в
состав некоторых компиляторов) не очень то случайны, когда речь
идет о младших битах. Попробуйте использовать старшие биты.
См. вопрос 12.9.
12.12 Я пытаюсь перенести на О: Эти подпрограммы в разной
другую систему старую степени устарели. Необходимо
программу. Почему я использовать
получаю сообщения
"undefined external"
для
index? используйте strchr.
rindex? используйте strrchr.
bcopy? используйте memmove,
поменяв местами первый
и второй аргументы (см. также
вопрос 5.15).
bcmp? используйте memcmp.
bzero? используйте memset, со вторым
аргументом, равным 0.
12.13 Я все время получаю сообщения об ошибках - не определены библиотечные
функции, но я включаю все необходимые головные файлы.
О: Иногда (особенно для нестандартных функций) следует явно указывать,
какие библиотеки нужны при компоновке программы.
См. также вопрос 15.2.
12.14 Я по-прежнему получаю сообщения, что библиотечные функции не
определены, хотя и использую ключ -l, чтобы явно указать библиотеки
во время компоновки.
О: Многие компоновщики делают один проход по списку объектных файлов
и библиотек, которые Вы указали, извлекая из библиотек только те
функции, удовлетворяющие ссылки, которые _к этому моменту_ оказались
неопределенными. Следовательно, порядок относительно объектных файлов,
в котором перечислены библиотеки, важен; обычно просмотр библиотек
нужно делать в самом конце. (Например, в операционной системе UNIX
помещайте ключи -l в самом конце командной строки).
12.15 Мне необходим исходный текст программы, которая осуществляет поиск
заданной строки.
О: Ищите библиотеку regesp (поставляется со многими UNIX - системами)
или достаньте пакет regexp Генри Спенсера (Henry Spencer)
(cs.toronto.edu директорий pub/regexp.shar.Z).
См. также вопрос 17.12.
12.16 Как разбить командную строку на разделенные пробельными литерами
аргументы (что-то вроде argc и argv в main)?
О: В большинстве компиляторов имеется функция strtok, хотя она
требует хитроумного обращения, а ее возможности могут Вас не
удовлетворить (например, работа в случае кавычек).
Смотри: ANSI Разд. 4.11.5.8; K&R II Разд.B3 c. 250; H&S
Разд. 15.7; PCS c. 178.
13. Lint.
13.1 Вот я написал программу, а она ведет себя странно. Что в ней не так?
О: Попробуйте сначала запустить lint (возможно, с ключами -a, -c, -h,
-p). Многие компиляторы С выполняют на самом деле только половину
задачи, не сообщая о тех подозрительных местах в тексте программы,
которые не препятствуют генерации кода.
13.2 Как мне подавить сообщение "warning: possible pointer alignment
problem" ("предупреждение: возможна проблема с выравниванием
указателя"), которое выдает lint после каждого вызова malloc?
О: Проблема состоит в том, что lint обычно не знает, и нет возможности
ему об этом сообщить, что malloc "возвращает указатель на область
памяти, которая должным образом выравнена для хранения объекта любого
типа". Возможна псевдореализация malloc с помощью #define внутри
#ifdef lint, которая удалит это сообщение, но слишком прямолинейное
применение #define может подавить и другие осмысленные сообщения о
действительно некорректных вызовах. Возможно, будет проще игнорировать
эти сообщения, может быть, делать это автоматически с помощью grep -v.
13.3 Где найти ANSI-совместимый lint?
О: Программа, которая называется FlexeLint (в виде исходного текста с
удаленными комментариями и переименованными переменными, пригодная
для компиляции на "почти любой" системе) может быть заказана по
адресу
Gimpel Software
3207 Hogarth Lane
Collegeville, PA 19426 USA
(+1) 610 584 4261
gimpel@netaxs.com
Lint для System V release 4 ANSI-совместим и может быть получен
(вместе с другими C утилитами) от UNIX Support Labs или от
дилеров System V.
Другой ANSI-совместимый LINT (способный также выполнять формальную
верификацию высокого уровня) называется LCLint и доступен через
ftp: larch.lcs.mit.edu://pub/Larch/lclint/ .
Ничего страшного, если программы lint нет. Многие современные
компиляторы почти столь же эфффективны в выявлении ошибок и
подозрительных мест, как и lint.
14. Стиль.
14.1 Может ли простой и приятный трюк
if(!strcmp(s1, s2))
служить образцом хорошего стиля?
О: Стиль не особенно хороший, хотя такая конструкция весьма популярна.
Тест удачен в случае равенства строк, хотя по виду условия можно
подумать, что это тест на неравенство.
Есть альтернативный прием, связанный с использованием макроса
#define Streq(s1, s2) (strcmp((s1), (s2)) == 0)
Вопросы стиля программирования, как и проблемы веры, могут
обсуждаться бесконечно. К хорошему стилю стоит стремиться, он легко
узнаваем, но не определим.
14.2 Каков наилучший стиль внешнего оформления программы?
О: K&R, приводя пример, которому чаще всего следуют, снабдили его
примечанием, предоставляющим Вам окончательный выбор:
Положение скобок менее важно, хотя люди склонны проявлять
фанатизм в таких вопросах. Мы выбрали один из нескольких
популярных стилей. Выберите тот стиль, который Вам больше
подходит, и точно ему следуйте.
Не так важно, чтобы стиль был "идеален". Важнее, чтобы он применялся
последовательно и был совместим (со стилем коллег или общедоступных
программ). Если требования к программированию (местные правила или
требования фирмы) не касаются стиля, и Вы не испытываете желания
изобрести свой собственный стиль, то просто следуйте K&R.
(Давайте не будем повторять бесконечные споры о преимуществах и
недостатках того или иного расположения отступов и скобок. См. также
Indian Hill Style Guide).
Так трудно определимое понятие "хороший стиль" включает в себя
гораздо больше, чем просто внешнее оформление программы; не тратьте
слишком много времени на отступы и скобки в ущерб более существенным
слагаемым качества.
Смотри: K&R Разд. 1.2 c. 10.
14.3 Где достать "Indian Hill Style Guide" и другие рекомендации по стилю
программирования?
О: Различные документы доступны через ftp:
Сервер: Файл или директорий:
cs.washington.edu ~ftp/pub/cstyle.tar.Z
(128.95.1.4) (переработанный Indian Hill guide)
cs.toronto.edu doc/programming
ftp.cs.umd.edu pub/style-guide
15. Операции с плавающей точкой.
15.1 У меня операции с плавающей точкой выполняются странно, и на разных
машинах получаются различные результаты.
О: Сначала убедитесь, что подключен головной файл <math.h> и правильно
объявлены другие функции, возвращающие тип double.
Если дело не в этом, вспомните, что большинство компьютеров используют
форматы с плавающей точкой, которые хотя и похоже, но вовсе не
идеально имитируют операции с действительными числами. Потеря
значимости, накопление ошибок и другие свойственные ЭВМ особенности
вычислений могут быть весьма болезненными.
Не нужно предполагать, что результаты операций с плавающей точкой
будут точными, в особенности не стоит проверять на равенство два числа
с плавающей точкой. (Следует избегать любых ненужных случайных
факторов.)
Все эти проблемы одинаково свойственны как С, так и другим языкам
программирования. Семантика операций с плавающей точкой определяется
обычно так, "как это выполняет процессор"; иначе компилятор вынужден
бы был заниматься непомерно дорогостоящей эмуляцией "правильной"
модели вычислений.
В этих вопросах и ответах нет возможности даже бегло перечислить все
затруднения при вычислениях с плавающей точкой и способы их
преодоления. Хорошая книга по программированию должна содержать
введение в эту область.
Смотри: EoPS Разд. 6 c. 115-8.
15.2 Я пытаюсь проделать кое-какие вычисления, связанные с тригонометрией,
включаю <math.h>, но все равно получаю сообщение: "undefined: _sin"
во время компиляции.
О: Убедитесь в том, что компоновщику известна библиотека, в которой
собраны математические функции. Например, в операционной системе
UNIX часто необходим ключ -lm в самом конце командной строки.
См. также вопрос 12.14.
15.3 Почему в языке С нет оператора возведения в степень?
О: Потому что немногие процессоры имеют такую инструкцию. Вместо этого
можно, включив головной файл <math.h>, использовать функцию pow(),
хотя часто при небольших целых порядках явное умножение
предпочтительней.
Смотри: ANSI Разд. 4.5.5.1 .
15.4 Как округлять числа?
А: Вот самый простой и честный способ:
(int)(x + 0.5)
Хотя для отрицательных чисел это не годится.
15.5 Как выявить специальное значение IEEE NaN и другие специальные
значения?
О: Многие компиляторы с высококачественной реализацией стандарта IEEE
операций с плавающей точкой обеспечивают возможность (например, макрос
isnan()) явной работы с такими значениями, а Numerical C Extensions
Group (NCEG) занимается стандартизацией таких средств. Примером
грубого, но обычно эффектиного способа проверки на NaN служит макрос
#define isnan(x) ((x) != (x))
хотя не знающие об IEEE компиляторы могут выбросить проверку в
процессе оптимизации.
15.6 У меня проблемы с компилятором Turbo C. Программа аварийно
завершается, выдавая нечто вроде "floating point formats not linked."
О: Некоторые компиляторы для мини-эвм, включая Turbo C (а также
компилятор Денниса Ритчи для PDP-11), не включают поддержку операций
с плавающей точкой, когда им кажется, что это не понадобится.
В особенности это касается версий printf и scanf, когда для экономии
места не включается поддержка %e, %f, и %g. Бывает так, что
эвристической процедуры Turbo C, которая определяет - использует
программа операции с плавающей точкой или нет, оказывается
недостаточно, и программист должен лишний раз вызвать функцию,
использующую операции с плавающей точкой, чтобы заставить компилятор
включить поддержку таких операций.
16. Интерфейс с операционной системой.
16.1 Как прочитать с клавиатуры один символ, не дожидаясь новой строки?
О: Вопреки популярному убеждению и желанию многих, этот вопрос (как и
родственные вопросы, связанные с дублированием символов) не относится
к языку С. Передача символов с "клавиатуры" программе, написанной на
С, осуществляется операционной системой, эта операция не
стандартизирована языком С. Некоторые версии библиотеки curses
содержат функцию cbreak(), которая делает как раз то, что нужно.
Если Вы пытаетесь прочитать пароль с клавиатуры без вывода его на
экран, попробуйте getpass(). В операционной системе UNIX используйте
ioctl для смены режима работы драйвера терминала(CBREAK или RAW для
"классических" версий; ICANON, c_cc[VMIN] и с_сс[VTIME] для System V
или Posix). В системе MS-DOS используйте getch(). В системе VMS
попробуйте функции управления экраном (SMG$) или curses, или
используйте низкоуровневые команды $QIO с кодами IO$_READVBLK (и,
может быть, IO$M_NOECHO) для приема одного символа за раз. В других
операционных системах выкручивайтесь сами. Помните, что в некоторых
операционных системах сделать нечто подобное невозможно, так как
работа с символами осуществляется вспомогательными процессорами и
не находится под контролем центрального процессора.
Вопросы, ответы на которые зависят от операционной системы,
неуместны в comp.lang.c. Ответы на многие вопросы можно найти в FAQ
таких групп как comp.unix.questions и comp.os.msdos.programmer.
Имейте в виду, что ответы могут отличаться даже в случае разных
вариантов одной и той же операционной системы. Если вопрос касается
специфики операционной системы, помните, что ответ, пригодный в Вашей
системе, может быть бесполезен всем остальным.
Смотри: PCS Разд. 10 c. 128-9, Разд. 10.1 c. 130-1.
16.2 Как определить - есть ли символы для чтения (и если есть, то сколько?)
И наоборот, как сделать, чтобы выполнение программы не блокировалось,
когда нет символов для чтения?
О: Ответ на эти вопросы также целиком зависит от операционной системы.
В некоторых версиях curses есть функция nodelay(). В зависимости
от операционной системы Вы сможете использовать "неблокирующий ввод/
вывод" или системный вызов "select" или ioctl FIONREAD, или kbhit(),
или rdchk(), или опцию O_NDELAY функций open() или fcntl().
16.3 Как очистить экран? Как выводить на экран негативное изображение?
О: Это зависит от типа терминала (или дисплея). Можете использовать
такую библиотеку как termcap или curses, или какие-то другие
функциии, пригодные для данной операционной системы.
16.4 Как узнать состояние мыши?
О: Посмотрите в системной документации, или поместите вопрос
(предварительно посмотрев их FAQ) в одну из групп, обсуждающих
определенную операционную систему. Работа с мышью совершенно непохожа
в X window, MS-DOS, Macintosh, и, возможно, в любой другой
операционной системе.
16.5 Как программа может определить полный путь к месту, из которого она
была вызвана?
О: argv[0] может содержать весь путь, часть его или ничего не содержать.
Если имя файла в argv[0] имеется, но информация не полна, возможно
повторение логики поиска исполнимого файла, используемой
интерпретатором командного языка. Гарантированных или мобильных
решений, однако, не существует.
16.6 Как процесс может изменить переменную окружения родительского
процесса?
О: В общем, никак. Различные операционные системы обеспечивают сходную с
UNIX возможность задания пары имя/значение. Может ли программа с
пользой для себя поменять окружение, и если да, то как - все это
зависит от операционной системы.
В системе UNIX процесс может модифицировать свое окружение
(в некоторых системах есть для этого функции setenv() и/или putenv())
и модифицированное окружение обычно передается дочерним процессам
но _не_ распространяется на родительский процесс.
16.7 Как проверить, существует ли файл? Мне необходимо спрашивать
пользователя перед тем как переписывать существующие файлы.
О: В UNIX-подобных операционных системах можно попробовать функцию
access(), хотя имеются кое-какие проблемы. (Применение access() может
сказаться на последующих действиях, кроме того, возможны особенности
исполнения в setuid-программах). Другое (возможно, лучшее) решение
- вызвать stat(), указав имя файла. Единственный универсальный,
гарантирующий мобильность способ состоит в попытке открыть файл.
16.8 Как определить размер файла до его чтения?
О: Если "размер файла" - это количество литер, которое можно прочитать,
то, вообще говоря, это количество заранее неизвестно. В операционной
системе Unix вызов функции stat дает точный ответ, и многие
операционные системы поддерживают похожую функцию, которая дает
приблизительный ответ. Можно c помощью fseek переместиться в конец
файла, а затем вызвать ftell, но такой прием немобилен (дает точный
ответ только в системе Unix, в других же случаях ответ почти точен
лишь для определенных стандартом ANSI "двоичных" файлов).
В некоторых системах имеются подпрограммы filesize или filelength.
И вообще, так ли нужно заранее знать размер файла? Ведь самый точный
способ определения его размера в С программе заключается в открытии и
чтении. Может быть, можно изменить программу так, что размер файла
будет получен в процессе чтения?
16.9 Как укоротить файл без уничтожения или переписывания?
О: В системах BSD есть функция ftruncate(), несколько других систем
поддерживают chsize(), в некоторых имеется (возможно,
недокументированный) параметр fcntl F_FREESP. В системе MS-DOS можно
иногда использовать write(fd, "", 0). Однако, полностью мобильного
решения не существует.
16.10 Как реализовать задержку или определить время реакции пользователя,
чтобы погрешность была меньше секунды?
О: У этой задачи нет, к несчастью, мобильных решений. Unix V7 и ее
производные имели весьма полезную функцию ftime() c точностью до
миллисекунды, но она исчезла в System V и Posix. Поищите такие
функции: nap(), setitimer(), msleep(), usleep(), clock(), и
gettimeofday(). Вызовы select() и poll() (если эти функции доступны)
могут быть добавлены к сервисным функциям для создания простых
задержек. В системе MS-DOS возможно перепрограммирование системного
таймера и прерываний таймера.
16.11 Как прочитать объектный файл и передать управление на одну из его
функций?
О: Необходим динамический компоновщик и/или загрузчик. Возможно выделить
память с помощью malloc и читать объектные файлы, но нужны обширные
познания в форматах объектных файлов, модификации адресов и пр.
В системе BSD Unix можно использовать system() и ld -A для
динамической компоновки. Mногие (большинство? ) версии SunOS и
System V имеют библиотеку -ldl, позволяющую динамически загружать
объектные модули. Есть еще GNU пакет, который называется "dld".
См. также вопрос 7.6.
16.12 Как выполнить из программы команду операционной системы?
О: Используйте system().
Смотри: K&R II Разд. B6 c. 253; ANSI Разд. 4.10.4.5; H&S
Разд. 21.2; PCS Разд. 11 c. 179;
16.13 Как перехватить то, что выдает команда операционной системы?
О: Unix и некоторые другие операционные системы имеют функцию popen(),
которая переназначает поток stdio каналу, связанному с процессом,
запустившим команду, что позволяет прочитать выходные данные (или
передать входные). А можно просто (см. вопрос 16.12) перенаправить
выход команды в файл, затем открыть его и прочесть.
Смотри: PCS Разд. 11 c. 169 .
16.14 Как получить содержимое директория в С программе?
О: Выясните, нельзя ли использовать функции opendir() и readdir(),
доступные в большинстве систем Unix. Реалиции этих функций известны
для MS-DOS, VMS и других систем. (MS-DOS имеет также функции
findfirst и findnext, которые делают в точности то же самое).
16.15 Как работать с последовательными (COM) портами?
О: Это зависит от операционной системы. В системе Unix обычно
осуществляются операции открытия, чтения и записи во внешнее
устройство и используются возможности терминального драйвера для
настройки характеристик. В системе MS-DOS можно либо использовать
прерывания BIOSa, либо (если требуется приличная скорость) один из
управляемых прерываниями пакетов для работы с последовательными
портами.
17. Разное.
17.1 Что можно с уверенностью сказать о начальных значениях переменных,
которые явным образом не инициализированы? Если глобальные переменные
имеют нулевое начальное значение, то правильно ли нулевое значение
присваивается указателям и переменным с плавающей точкой?
А: "Cтатические" переменные (то есть объявленные вне функций и те, что
объявлены как принадлежащие классу stаtic) всегда инициализируются
(прямо при старте программы) нулем, как будто программист написал
"=0". Значит, переменные будут инициализированы как нулевые указатели
(соответствующего типа; см. раздел 1), если они объявлены указателями,
или значениями 0.0, если были объявлены переменные с плавающей точкой.
Переменные автоматического класса (т.е. локальные переменные без
спецификации static), если они явно не определены, первоначально
содержат "мусор". Никаких полезных предсказаний относительно мусора
сделать нельзя.
Память, динамически выделяемая с помощью malloc и realloc также
будет содержать мусор и должна быть инициализирована, если это
необходимо, вызывающей программой. Память, выделенная с помощью
calloc, зануляет все биты, что не всегда годится для указателей или
переменных с плавающей точкой (см. вопрос 3.13 и раздел 1).
17.2 Этот текст взят прямо из книги, но он не компилируется.
f()
{
char a[] = "Hello, world!";
}
О: Возможно, Ваш компилятор создан до принятия стандарта ANSI и
еще не поддерживает инициализацию "автоматических агрегатов"
(то есть нестатических локальных массивов и структур).
Чтобы выкрутиться из этой ситуации, сделайте массив статическим или
глобальным, или инициализируйте его с помощью strcpy, когда
вызывается f(). (Всегда можно инициализировать автоматическую
переменную char * стрингом литер, но см. вопрос 17.20). См. также
вопросы 5.16, 5.17.
17.3 Как писать данные в файл, чтобы их можно было читать на машинах
с другим размером слова, порядком байтов или другим форматом чисел
с плавающей точкой?
О: Лучшее решение - использовать текстовые файлы (обычно ASCII),
c данными, записанными fprintf. Читать данные лучше всего с
помощью fscanf или чего-то подобного. (Такой же совет применим
для сетевых протоколов). К мнениям, что текстовые файлы слишком
велики и могут долго обрабатываться, относитесь скептически.
Помимо того, что эффективность таких операций может быть на практике
приемлемой, способность манипулировать данными с помощью стандартных
средств может иметь решающее значение.
Если необходимо использовать двоичный формат, переносимость данных
можно улучшить (или получить выгоду от использования готовых
библиотек ввода/вывода), если использовать стандартные форматы данных,
такие как XDR (RFC 1014) (Sun), ASN.1(OSI), X.409 (CCITT), или ISO
8825 "Основные правила кодирования" См. также вопрос 9.11.
17.4 Как вставить или удалить строку (или запись) в середине файла?
О: Придется, видимо, переписать файл. См. вопрос 16.9.
17.5 Как возвратить из функции несколько значений?
О: Или передайте указатель на то место, которое будет заполнено функцией,
или пусть функция возвращает структуру, содержащую желаемые значения,
или подумайте о глобальных переменных (если их немного). См. также
вопросы 2.17, 3.4, и 9.2.
17.6 Если есть указатель (char *) на имя функции в виде стринга, то как
эту функцию вызвать?
О: Наиболее прямолинейный путь - создание таблицы имен и соответствующих
им указателей:
int function1(), function2();
struct {char *name; int (*funcptr)(); } symtab[] =
{
"function1", function1,
"function2", function2,
};
Ну а теперь нужно поискать в таблице нужное имя и вызвать функцию,
используя связанный с именем указатель. См. также вопросы 9.9 и 16.11.
17.7 У меня, кажется, нет головного файла <sgtty.h>. Пришлите мне его,
пожалуйста.
О: Стандартные головные файлы существуют в том смысле, что содержат
информацию, необходимую компилятору, операционной системе и
процессору. "Чужой" головной файл подойдет лишь тогда, когда взят из
идентичного окружения. Поинтересуйтесь у продавца компилятора, почему
отсутствует головной файл, или попросите прислать новый взамен
потерянного.
17.8 Как вызвать процедуры, написанные на языке FORTRAN (C++,BASIC,Pascal,
Ada, Lisp) из С (и наоборот).
О: Ответ полностью зависит от машины и от специфики передачи параметров
различными компиляторами. Решения вообще может не быть. Внимательно
читайте руководство по компилятору. Иногда в документации имеется
"Руководство по смешанному программированию", хотя техника передачи
аргументов и обеспечения правильного входа в функцию зачастую весьма
таинственна. Дополнительная информация находится в файле FORT.gz
Глена Гирса, (Glenn Geers) который можно получить с помощью ftp
suphys.physics.su.oz.au в директории src.
Головной файл cfortran.h упрощает взаимодействие C/FORTRAN на многих
популярных машинах. cfortran.h можно получит через ftp
zebra.desy.de (131.169.2.244).
В C++ модификатор "C" внешней функции показывает, что функция будет
вызываться с использованием соглашения о передаче параметров языка
С.
17.9 Кто-нибудь знает о программах, переводящих Pascal или FORTRAN
(или LISP, Ada, awk, "старый" С) в С?
О: Есть несколько общедоступных программ:
p2c Переводчик с Паскаля на С, написанный Дейвом Гиллеспи,
(Dave Gillespie) помещен в comp.sources.unix в Марте 1990
(Volume 21); доступен также через ftp csvax.cs.caltech.edu,
файл pub/p2c-1.20.tar.Z .
ptoc Другой переводчик с Паскаля на С, написан на Паскале
(comp.sources.unix, Volume 10, поправки в vol. 13?)
f2c Переводчик с фортрана на С совместно разработанный
Bell Labs, Bellcore, and Carnegie Mellon. Подробности
можно получить, послав электронной почтой сообщение
"send index from f2c" по адресу netlib@research.att.com
или research!netlib.
(Эти подробности можно получить и через ftp
netlib.att.com, в директории netlib/f2c.)
Составитель этого списка вопросов и ответов имеет список других
коммерческих трансляторов, среди них трансляторы для менее известных
языков. См. также вопрос 5.3.
17.10 Правда ли, что C++ - надмножество С. Можно ли использовать
компилятор C++ для трансляции C программ?
О: С++ вырос из С и в большой степени базируется на нем, но некоторые
правильные конструкции С недопустимы в C++. (Многие С программы,
будут, тем не менее, правильно транслироваться компилятором С++).
17.11 Мне нужен: О: Ищи программы, которые
называются (см. также вопрос 17.12)
генератор перекрестных cflow, calls, cscope
ссылок С
C форматизатор cb, indent
17.12 Где найти все эти общедоступные программы?
О: Если у Вас есть доступ к Usenet, смотрите периодически помещаемые
сообщения в comp.sources.unix и comp.sources.misc, которые описывают
некоторые детали ведения архивов и подсказывают, как получить те
или иные файлы. Обычно используется ftp и/или uucp c центральным,
ориентированным на пользователей сервером, таким как uunet
(ftp.uu.net, 192.48.96.9). Однако, в этих вопросах и ответах
невозможно исследовать или перечислить все архивные серверы и
рассказать о доступе к ним.
Ай Ша (Ajay Shah) поддерживает список общедоступных программ в
области численного анализа, который периодически публикуется,
и его можно найти там же, где и данные вопросы и ответы (см.
вопрос 17.33). Группа Usenet comp.archives содержит многочисленные
объявления о том, что доступно на различных ftp. Почтовый сервер
"archie" может подсказать, на каком ftp имеются те или иные программы.
Пошлите почтовое сообщение "help" по адресу
archie@quiche.cs.mcgill.ca для получения дальнейших инструкций.
Наконец, группа comp.sources.wanted - обычно самое подходящее место,
где можно поместить соответствующий запрос, но посмотрите прежде _их_
список вопросов и ответов (FAQ) "Как найти источники".
17.13 Где состоятся следующие Соревнования по Непонятному С
Программированию (International Obfuscated C Code Contest - IOCCC)?
Как получить программы, победившие в текущем и прошлых конкурсах?
О: Соревнования обычно проходят с начала марта до середины мая. Для
получения правил и рекомендаций, касающихся участия в конкурсе,
пошлите электронной почтой письмо по адресу:
{apple,pyramid,sun,uunet}!hoptoad!judges или
judges@toad.com
,в разделе Subject которого напишите "send rules".
Имейте в виду, что это _не_ адрес, куда нужно посылать конкурсные
программы.
Победители конкурса сначала объявляются на летней конференции
Usenix (Summer Usenix Conference) в середине июня и становятся
доступными в сети где-то в июле-августе. Программы-победители
прошлых конкурсов (начиная с 1984 г.) заархивированы в uunet (см.
вопрос 17.12) в директории ~/pub/ioccc.
В крайнем случае программы-победители прошлых конкурсов можно
получить, послав по указанному выше адресу письмо в графе Subject
которого нужно указать: "send YEAR winners", где YEAR - год,
представленный четырьмя цифрами, интервал в несколько лет или "all".
17.14 Почему недопустимы вложенные комментарии? Как прикажете
"выключить" фрагмент программы, в котором уже есть комментарии?
Можно ли использовать комментарии внутри стринговых констант?
О: Вложенные комментарии принесут больше вреда, чем пользы, главным
образом из-за возможности случайно не закрыть комментарий, оставив
внутри него символы "/*". По этой причине лучше "выключить" большой
фрагмент программы, в котором уже есть комментарии, с помощью средств
препроцессора #ifdef или #if 0 (но имейте в виду вопрос 5.11).
Последовательность символов /* и */ не имеет специального значения
внутри заключенных в двойные кавычки стрингов. Эта последовательность
не рассматривается как комментарий, поскольку программа (особенно
та, которая создает текст другой С програмы) должна иметь
возможность эти комментарии печатать.
Смотри: ANSI Appendix E p. 198, Rationale Разд. 3.1.9 p. 33.
17.15 Как получить значение кода ASCII той или иной литеры, и наоборот?
О: В С литеры представлены целыми числами, соответствующими их значениям.
(в соответствии с набором символов данной машины). Так что нет
необходимости в преобразовании: если известна литера, то известно
и ее значение.
17.16 Как реализовать последовательности и/или массивы бит?
О: Используйте массивы переменных типа char или int и несколько
макросов для операций с отдельными битами (используйте определение
8 для CHAR_BIT, если нет головного файла <limits.h>:
#include <limits.h> /* для CHAR_BIT */
#define BITMASK(bit) (1 << ((bit) % CHAR_BIT))
#define BITSLOT(bit) ((bit) / CHAR_BIT)
#define BITSET(ary, bit) ((ary)[BITSLOT(bit)] |= BITMASK(bit))
#define BITTEST(ary, bit) ((ary)[BITSLOT(bit)] & BITMASK(bit))
17.17 Как наилучшим образом определить число установленных бит,
соответствующих определенному значению?
О: Решение этой и многих других проблем из области битоверчения можно
ускорить и сделать более эффективным с помощью таблиц перекодировки.
(но имейте в виду следующий вопрос).
17.18 Как повысить эффективность работы программы?
О: Тема эффективности, очень часто затрагиваемая в comp.lang.c,
не так важна как многие склонны думать. Большая часть кода в
большинстве программ не влияет на время исполнения. Если время,
занимаемое каким-то участком кода, мало по сравнению с общим
временем исполнения, то для этого участка гораздо важнее простота
и мобильность, чем эффективность. (Помните, что компьютеры очень,
очень быстры и даже "неэффективный" участок кода может выполняться
без видимой задержки).
Печально известны попытки предсказать "горячие точки" программы.
Когда эффективность программы имеет значение, важно использовать
профилировщики для определения тех участков программы, которые
заслуживают внимания. Часто основное время выполнения поглощается
периферийными операциями, такими как ввод/вывод и выделение памяти,
которые можно ускорить с помощью буферизации и хеширования.
Для небольших участков программы, критичных в смысле эффективности,
жизненно важно выбрать подходящий алгоритм; "микрооптимизация"
этого участка менее важна. Многие часто предлагаемые "приемы по
увеличению эффективности" (вроде замены операции сдвига умножением
на степень двойки) выполняются автоматически даже неизощренными
компиляторами.
Неуклюжие попытки оптимизации способны так увеличить размер
программы, что ее эффективность упадет.
Дальнейшее обсуждение противоречий, связанных с эффективностью, а так
же хорошие советы по увеличению эффективности, когда это важно,
смотрите в главе 7 книги Кернигана и Плоджера "Элементы стиля
программирования", а также в книге Джона Бентли "Написание
эффективных программ".
17.19 Правда ли, что применение указателей более эффективно, чем
применение массивов? Насколько замедляет программу вызов функции?
Быстрее ли ++i чем i = i + 1?
О: Точные ответы на эти и многие другие похожие вопросы, конечно же,
зависят от процессора и применяемого компилятора. Если знать это
необходимо, придется аккуратно определить время выполнения тестовых
программ. (Часто различия столь незначительны, что потребуются сотни
тысяч повторений, чтобы их увидеть. Если есть возможность,
посмотрите ассемблерный листинг, выдаваемый компилятором, чтобы
убедиться в различной трансляции двух претендующих на первенство
альтернатив).
"Обычно" быстрее продвигаться по большим массивам с помощью
указателей, чем с помощью индексов, однако есть процессоры, для
которых справедливо обратное.
Хотя вызовы функций и увеличивают время выполнения, сами функции
настолько повышают модульность и простоту понимания программы,
что едва ли полезно от них отказываться.
Прежде чем переписывать выражения типа i=i+1, вспомните, что имеете
дело с компилятором С а не с программируемым калькулятором. Любой
приличный компилятор будет одинаково транслировать ++i,i+=1; i=i+1.
Использовать ++i, i+=1 или i=i+1 - вопрос стиля, не эффективности.
(См. также вопрос 4.7).
17.20 Почему не выполняется такой фрагмент?
char *p = "Hello, world!";
p[0] = tolower(p[0]);
О: Стринговые константы не всегда можно модифицировать, за исключением
случая, когда ими инициализируется массив. Попробуйте
char a[] = "Hello, world!";
(Для компиляции старых программ некоторые компиляторы имеют ключ,
который управляет возможностью модификации стринговых констант.)
См. также вопросы 2.1,2.2, 2.8 и 17.2.
Смотри: ANSI Разд. 3.1.4 .
17.21 Моя программа аварийно завершается еще до выполнения! (если
использовать отладчик, то видно, что смерть наступает еще до
выполнения первой инструкции в main).
О: Видимо, у Вас один или несколько очень больших (более килобайта)
локальных массивов. Во многих системах размер стека фиксирован,
а операционные системы, в которых осуществляется динамическое
выделение стековой памяти, (например, UNIX) могут быть введены в
заблуждение, когда размер стека резко увеличивается.
Часто предпочтительнее объявить большие массивы типа static(если,
конечно, каждый раз при рекурсивном вызове не требуется свежий
массив).
(См. также вопрос 9.4).
17.22 Что означают сообщения "Segmentation violation" и "Bus error" ?
О: Это значит, что программа пытается получить доступ к несуществующей
или запрещенной для нее области памяти. Это постоянно происходит
из-за неинициализированных или неверно инициалированных указателей
(см. вопросы 3.1, 3.2), по вине malloc (см. вопрос 17.23) или, может
быть, scanf (см. вопрос 11.3).
17.23 Моя программа аварийно завершается, очевидно, при выполнении malloc,
но я не вижу в ней ничего плохого.
О: К несчастью, очень легко разрушить внутренние структуры данных,
создаваемые malloc, а возникающие проблемы могут быть трудны для
отладки. Чаще всего проблемы возникают при попытке записать больше
данных, чем может уместиться в памяти, выделенной malloc; особенно
распространена ошибка malloc(strlen(s)) вместо strlen(s) + 1.
Другие проблемы включают освобождение указателей, полученных не
в результате выполнения malloc, или попытки применить функцию
realloc к нулевому указателю. (см. вопрос 3.12).
Существует несколько отладочных пакетов, чтобы помочь отследить
возникающие при применении malloc проблемы. Один из популярных -
"dbmalloc" Конора П. Кахилла, (Conor P. Cahill) помещенный в
comp.sources.misc в сентябре 1992. Другие - это "leak" помещенный в
том 27 архива comp.sources.unix, JMalloc.c и JMalloc.h в сборике
Fidonet Snippets (ищите с помощью archie; см. также вопрос 17.2); и
MEMDEBUG - см. ftp.crpht.lu в pub/sources/memdebug .
См. также вопрос 17.12.
17.24 Есть у кого-нибудь комплект тестов для С компилятора?
О: Плюм Холл (Plum Hall) (ранее работавший в Кардифе, Нью Джерси, теперь
Гаваи) продает такой комплект. Дистрибутив GNU C (gcc) от FSF
включает c-torture-test.tar.Z который выявляет многие проблемы,
возникающие при использованиии компиляторов. Тест Кагана (Kahan) под
названием paranoia, который находится в директории netlib/paranoia на
netlib.att.com интенсивно тестирует операции с плавающей точкой.
17.25 Где достать грамматику С для программы YACC?
О: Самая надежная - конечно же грамматика из стандарта ANSI. Другая
грамматика, подготовленная Джимом Роскиндом (Jim Roskind), находится
на ics.uci.edu в директории pub/*grammar*. Одетый в плоть,
работающий образец ANSI грамматики (принадлежащий Джефу Ли(Jeff Lee))
находится на uunet (см. вопрос 17.12) в директории
usenet/net.sources/ansi.c.grammar.Z (вместе с лексическим
анализатором). В компиляторе GNU C от FSF есть грамматика, так же как
есть она в приложении к книге K&R II.
Смотри: ANSI Разд. A.2 .
17.26 Мне необходим исходный текст для разбора и вычисления формул.
О: Есть два доступных пакета - "defunc" , помещенный в comp.source.misc
в декабре 1993 г. (V41 i32,33), в alt.sources в январе 1994 г.,
его можно получить через ftp sunsite.unc.edu в директории
pub/packages/development/libraries/defunc-1.3.tar.Z; и
пакет "parse" в lamont.ldgo.columbia.edu.
17.27 Мне необходима функция типа strcmp, но для приблизительного
сравнения, чтобы проверить две строки на близость, но не на
тождество.
О: Обычно такие сравнения включают алгоритм "soundex", который
ставит в соответствие сходно звучащим словам один и тот же числовой
код. Этот алгоритм описан в томе "Сортировка и поиск" классической
книги Дональда Кнута "Искусство программирования для ЭВМ".
17.28 Как по дате найти день недели?
О: Используйте mktime (см. вопросы 12.6 и 12.7) или соотношение
Зеллера (Zeller), или попробуйте вот эту функцию, помещенную Томохико
Сакамото (Tomohiko Sakamoto):
dayofweek(y, m, d) /* 0 = Воскресенье */
int y, m, d; /* 1 <= m <= 12, y > 1752 (примерно) */
{
static int t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
y -= m < 3;
return (y + y/4 - y/100 + y/400 + t[m-1] + d) % 7;
}
17.29 2000-й год будет високосным? (год %4 ==0) - правильный тест на
високосный год?
О: Да и нет соответственно. Вот полной тест для Григорианского
календаря:
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
17.30 Как произносить "char"?
О: Ключевое слово С "char" можно произносит тремя способами:
как английские слова "char," "care," or "car;". Выбор за Вами.
17.31 Какие есть хорошие книги для изучения С?
О: Митч Райт (Mitch Wright) поддерживает аннотированную библиографию
книг по С и по UNIX; она доступна через ftp ftp.rahul.net в
директории pub/mitch/YABL.
17.32 Можно ли получить книги по С через Интернет?
О: Можно, по крайней мере, две:
"Notes for C programmers," Кристофера Соутелла,
(Christopher Sawtell) доступна через
svr-ftp.eng.cam.ac.uk:misc/sawtell_C.shar
garbo.uwasa.fi:/pc/c-lang/c-lesson.zip
paris7.jussieu.fr:/contributions/docs
"C for Programmers," Тима Лова (Tim Love)
доступна через
svr-ftp.eng.cam.ac.uk в директории misc.
17.33 Где найти другие варианты этих вопросов и ответов? Доступны
ли более ранние редакции?
О: Пошарьте по Сети. Обычно эти вопросы и ответы помещаются в
comp.lang.c первого числа каждого месяца, со значением поля Expires,
позволяющим присутствовать в comp.lang.c весь месяц. Там же
есть сокращенная версия, представляющая собой список изменений,
сопровождающий существенно обновленную версию. Такие списки можно
так же найти в comp.answers и news.answers. Несколько серверов ведут
архивы сообщений, помещаемых в news.answers, а также списков часто
задаваемых вопросов (FAQ), включая и этот. Вот два сервера
rtfm.mit.edu (директории pub/usenet/news.answers/C-faq/ и
pub/usenet/comp.lang.c/ ) и ftp.uu.net (директорий
usenet/news.answers/C-faq/ ). Сервер archie должен помочь найти
другие архивы; сделайти запрос на "prog C-faq". Дополнительную
информацию можно найти в списке meta-FAQ в news.answers; см. также
вопрос 17.12.
Этот документ - постоянныо расширяющийся список вопросов, которые
часто задавались со времен Великого Переименования, а не коллекция
вопросов, которые были интересны в этом месяце. Предыдущие версии
этого документа устарели, и в них нет ничего интересного за
исключением случайных орфографических ошибок, которых в данной версии
уже нет.
Библиграфия
ANSI American National Standard for Information Systems --
Programming Language -- C, ANSI X3.159-1989 (см. вопрос 5.2).
JLB Jon Louis Bentley, Writing Efficient Programs, Prentice-Hall,
1982, ISBN 0-13-970244-X.
H&S Samuel P. Harbison and Guy L. Steele, C: A Reference Manual,
Second Edition, Prentice-Hall, 1987, ISBN 0-13-109802-0.
(скоро появится третье издание.)
PCS Mark R. Horton, Portable C Software, Prentice Hall, 1990,
ISBN 0-13-868050-7.
EoPS Brian W. Kernighan and P.J. Plauger, The Elements of Programming
Style, Second Edition, McGraw-Hill, 1978, ISBN 0-07-034207-5.
Русский перевод: Б.Керниган,Ф.Плоджер "Элементы стиля
программирования", Радио и связь, 1984, 160 стр.
K&R I Brian W. Kernighan and Dennis M. Ritchie, The C Programming
Language, Prentice-Hall, 1978, ISBN 0-13-110163-3.
Русский перевод: Керниган Б., Ритчи Д. "Язык программирования
С", М.: Финансы и статистика,1985.
K&R II Brian W. Kernighan and Dennis M. Ritchie, The C Programming
Language, Second Edition, Prentice Hall, 1988, ISBN 0-13-
110362-8, 0-13-110370-9.
Русский перевод:Б.Керниган, Д.Ритчи "Язык программирования С",
М.: Финансы и статистика, 1992.
Knuth Donald E. Knuth, The Art of Computer Programming, (3 vols.),
Addison-Wesley, 1981.
Русский перевод: Д.Кнут "Искусство программирования для ЭВМ",
т.1 Основные алгоритмы (М.:Мир,1976),т.2 Получисленные алгоритмы
(М.:Мир,1977), т.3 "Сортировка и поиск", (М.:Мир, 1978)
CT&P Andrew Koenig, C Traps and Pitfalls, Addison-Wesley, 1989,
ISBN 0-201-17928-8.
P.J. Plauger, The Standard C Library, Prentice Hall, 1992,
ISBN 0-13-131509-9.
Harry Rabinowitz and Chaim Schaap, Portable C, Prentice-Hall,
1990, ISBN 0-13-685967-4.
Более подробную библиографию можно найти в переработанном руководстве
по стилю Indian Hill (см. вопрос 14.3, а также вопрос 17.31).
Благодарности
Спасибо следующим людям:
Jamshid Afshar, Sudheer Apte, Randall Atkinson, Dan Bernstein,
Vincent Broman, Stan Brown, Joe Buehler, Gordon Burditt, Burkhard Burow,
Conor P. Cahill, D'Arcy J.M. Cain, Christopher Calabrese, Ian Cargill,
Paul Carter, Billy Chambless, Raymond Chen, Jonathan Coxhead, Lee
Crawford, Steve Dahmer, Andrew Daviel, James Davies, Jutta Degener, Norm
Diamond, Jeff Dunlop, Ray Dunn, Stephen M. Dunn, Michael J. Eager, Dave
Eisen, Bjorn Engsig, Chris Flatters, Rod Flores, Alexander Forst, Jeff
Francis, Dave Gillespie, Samuel Goldstein, Alasdair Grant, Ron
Guilmette, Doug Gwyn, Tony Hansen, Joe Harrington, Guy Harris, Elliotte
Rusty Harold, Jos Horsmeier, Blair Houghton, Ke Jin, Kirk Johnson, Larry
Jones, Kin-ichi Kitano, Peter Klausler, Andrew Koenig, Tom Koenig, Ajoy
Krishnan T, Markus Kuhn, John Lauro, Felix Lee, Mike Lee, Timothy J.
Lee, Tony Lee, Don Libes, Christopher Lott, Tim Love, Tim McDaniel,
Stuart MacMartin, John R. MacMillan, Bob Makowski, Evan Manning, Barry
Margolin, George Matas, Brad Mears, Bill Mitchell, Mark Moraes, Darren
Morby, Ken Nakata, Landon Curt Noll, David O'Brien, Richard A. O'Keefe,
Hans Olsson, Philip (lijnzaad@embl-heidelberg.de), Andrew Phillips,
Christopher Phillips, Francois Pinard, Dan Pop, Kevin D. Quitt, Pat
Rankin, J. M. Rosenstock, Erkki Ruohtula, Tomohiko Sakamoto, Rich Salz,
Chip Salzenberg, Paul Sand, DaviD W. Sanderson, Christopher Sawtell,
Paul Schlyter, Doug Schmidt, Rene Schmit, Russell Schulz, Patricia
Shanahan, Peter da Silva, Joshua Simons, Henry Spencer, David Spuler,
Melanie Summit, Erik Talvola, Clarke Thatcher, Wayne Throop, Chris
Torek, Andrew Tucker, Goran Uddeborg, Rodrigo Vanegas, Jim Van Zandt,
Wietse Venema, Ed Vielmetti, Larry Virden, Chris Volpe, Mark Warren,
Larry Weiss, Freek Wiedijk, Lars Wirzenius, Dave Wolverton, Mitch
Wright, Conway Yee, и Zhuo Zang, кто явно или неявно внес вклад в этот
документ.
Отдельно хочу поблагодарить Карла Хойера (Karl Heuer) и в особенности
Марка Бредера (Mark Brader), кто, говоря словами Стива Джонсона,
(Steve Johnson) "в безжалостном стремлении улучшить этот документ
уводил меня не только за пределы моих желаний, но и за пределы
моих возможностей".
Стив Саммит (Steve Summit)
Авторские права на этот документ принадлежат Стиву Саммиту (Steve Summit).
Документ можно свободно распространять пока в нем
присутствует имя автора и эти строки. Отрывки С программ в этом документе
(vstrcat(), error(), и т.д.) относятся к общедоступным и могут использоваться
без ограничений.
Послесловие переводчика
Отдельные фрагменты "Вопросов и ответов по языку С" я начал
переводить "для себя", но логика работы заставила меня перевести весь
документ от начала до конца.
Язык C в вопросах и ответах
Некоторые вопросы появляются вновь и вновь в этой конференции. Это
хорошие вопросы, и ответы на них могут быть далеко не очевидны, но каждый раз
ресурсы Сети и время читателя тратятся на повторяющиеся отклики и на нудные
поправки к некорректным ответам, возникновение которых неизбежно.
В этом документе, публикуемом ежемесячно, делается попытка ответить на
такие вопросы ясно и кратко, чтобы обсуждения стали более плодотворными, а
не возвращались постоянно к основным принципам.
Никакой список вопросов и ответов не заменит тщательного изучения
хорошего учебника или справочника по языку С. Тому, кто с интересом участвует
в этой конференции, должно быть также интересно прочесть одну или несколько
таких книг, желательно не один раз. Качество некоторых книг и руководств по
компилятору нельзя, к сожалению, назвать высоким; есть в них и попытки
увековечить некоторые мифы, которые данный документ пытается развеять.
Несколько заслуживающих внимания книг по С перечислены в библиографии.
Многие вопросы и ответы содержат ссылки на эти книги для дальнейшего изучения
интересующимся и увлеченным читателем. (Но помните о различной нумерации
документов ANSI и ISO стандартов С; см. вопрос 5.1).
Если у Вас есть вопрос, касающийся языка С, на который нет ответа в данном
документе, попытайтесь получить ответ на него из перечисленных здесь книг,
или спросите у знающих коллег, прежде чем использовать Сеть. Многие будут
счастливы ответить на ваши вопросы, но количество повторяющихся ответов на
один и тот же вопрос, как и возрастающее по мере привлечения читателей число
вопросов, может стать угнетающим. Если у Вас есть вопросы или замечания по
этому документу, используйте, пожалуйста, электронную почту -- эти вопросы и
ответы призваны снизить нагрузку на Сеть, а не увеличить ее.
Кроме списка наиболее часто задаваемых вопросов, в этом документе суммированы
наиболее часто даваемые ответы. Даже если Вы знаток языка С, полезно найти
время, чтобы бегло просмотреть этот документ, и тогда Вам не придется зря
тратить время в случае, когда кто-то поместил вопрос, ответ на который
уже дан.
Ответы на вопросы разбиты по темам:
1. Нулевые указатели
2. Указатели и массивы
3. Выделение памяти
4. Выражения
5. ANSI C
6. Препроцессор С
7. Списки аргументов переменной длины
8. Булевы выражения и переменные
9. Структуры, перечисления и объединения
10. Декларации
11. Cтандартный ввод/вывод
12. Библиотечные функции
13. Lint
14. Стиль
15. Операции с плавающей точкой
16. Интерфейс с операционной системой
17. Разное (Пребразование Fortran -> C , грамматики для YACC и т.п.)
К сему прилагаются часто задаваемые вопросы и ответы на них.
1. Нулевые указатели
1.1: Расскажите все-таки о пресловутых нулевых указателях.
О: Для каждого типа указателей существует (согласно определению языка)
особое значение - "нулевой указатель", которое отлично от всех других
значений и не указывает на какой-либо объект или функцию. Таким
образом, ни оператор &, ни успешный вызов malloc() никогда не
приведут к появлению нулевого указателя. (malloc возвращает нулевой
указатель, когда память выделить не удается, и это типичный пример
использования нулевых указателей как особых величин, имеющих
несколько иной смысл "память не выделена" или "теперь ни на что не
указываю". )
Нулевой указатель принципиально отличается от неинициализированного
указателя. Известно, что нулевой указатель не ссылается ни на какой
объект; неинициализированный указатель может ссылаться на что угодно.
См. также вопросы 3.1, 3.13, и 17.1.
В приведенном выше определении уже упоминалось, что существует
нулевой указатель для каждого типа указателя, и внутренние значения
нулевых указателей разных типов могут отличаться. Хотя программистам
не обязательно знать внутренние значения, компилятору всегда
необходима информация о типе указателя, чтобы различить нулевые
указатели, когда это нужно (см. ниже).
Смотри: K&R I Разд. 5.4 c. 97-8; K&R II Разд. 5.4 c. 102; H&S
Разд. 5.3 c. 91; ANSI Разд. 3.2.2.3 c. 38.
1.2: Как "получить" нулевой указатель в программе?
О: В языке С константа 0, когда она распознается как указатель,
преобразуется компилятором в нулевой указатель. То есть, если во
время инициализации, присваивания или сравнения с одной стороны
стоит переменная или выражение, имеющее тип указателя, компилятор
решает, что константа 0 с другой стороны должна превратиться в
нулевой указатель и генерирует нулевой указатель нужного типа.
Следовательно, следующий фрагмент абсолютно корректен:
char *p = 0;
if(p != 0)
Однако, аргумент, передаваемый функции, не обязательно будет
распознан как значение указателя, и компилятор может оказаться не
способным распознать голый 0 как нулевой указатель. Например,
системный вызов UNIX "execl" использует в качестве параметров
переменное количество указателей на аргументы, завершаемое нулевым
указателем. Чтобы получить нулевой указатель при вызове функции,
обычно необходимо явное приведение типов, чтобы 0 воспринимался как
нулевой указатель.
execl("/bin/sh", "sh", "-c", "ls", (char *)0);
Если не делать преобразования (char *), компилятор не поймет,
что необходимо передать нулевой указатель и вместо этого передаст
число 0. (Заметьте, что многие руководства по UNIX неправильно
объясняют этот пример.)
Когда прототипы функций находятся в области видимости, передача
аргументов идет в соответствии с прототипом и большинство приведений
типов может быть опущено, так как прототип указывает компилятору, что
необходим указатель определенного типа, давая возможность правильно
преобразовать нули в указатели. Прототипы функций не могут, однако,
обеспечить правильное преобразование типов в случае, когда функция
имеет список аргументов переменной длины, так что для таких
аргументов необходимы явные преобразования типов. Всегда безопаснее
явные преобразования в нулевой указатель, чтобы не наткнуться на
функцию с переменным числом аргументов или на функцию без прототипа,
чтобы временно использовать не-ANSI компиляторы, чтобы
продемонстрировать, что Вы знаете, что делаете. (Кстати, самое
простое правило для запоминания.)
Итог:
Можно использовать 0 Необходимо преобразование
инициализация вызов функции, прототип которой
вне области видимости
присваивание переменное число аргументов
при вызове функции
сравнение
вызов функции, прототип
в области видимости,
количество аргументов
фиксировано
Смотри: K&R I Разд. A7.7 c. 190, Разд. A7.14 c. 192; K&R II
Разд. A7.10 c. 207, Разд. A7.17 c. 209; H&S Разд. 4.6.3 c. 72; ANSI
Разд. 3.2.2.3 .
1.3 Что такое NULL и как он определен с помощью #define?
O: Многим программистам не нравятся нули, беспорядочно разбросанные по
программам. По этой причине макрос препроцессора NULL определен в
<stdio.h> или <stddef.h> как значение 0 (или (void *) 0, об этом
значении поговорим позже.) Программист, который хочет явно различать
0 как целое и 0 как нулевой указатель может использовать NULL в тех
местах, где необходим нулевой указатель. Это только стилистическое
соглашение; препроцессор преобразует NULL опять в 0, который затем
распознается компилятором в соответствующем контексте как нулевой
указатель. В отдельных случаях при передаче параметров функции,
может все же потребоваться явное указание типа перед NULL (как и
перед 0). (Таблица в вопросе 1.2 приложима как к NULL, так и к 0).
NULL нужно использовать _только_ для указателей; см. вопрос 1.8.
Смотри: K&R I Разд. 5.4 c. 97-8; K&R II Разд. 5.4 c. 102; H&S
Разд. 13.1 c. 283; ANSI Разд. 4.1.5 c. 99, Разд. 3.2.2.3 c. 38,
Rationale Разд. 4.1.5 c. 74.
1.4: Как #define должен определять NULL на машинах, использующих
ненулевой двоичный код для внутреннего представления нулевого
указателя?
О: Программистам нет необходимости знать внутреннее представление(я)
нулевых указателей, ведь об этом обычно заботится компилятор.
Если машина использует ненулевой код для представления нулевых
указателей, на совести компилятора генерировать этот код, когда
программист обозначает нулевой указатель как "0" или NULL.
Следовательно, определение NULL как 0 на машине, для которой нулевые
указатели представляются ненулевыми значениями так же правомерно как
и на любой другой, так как компилятор должен (и может) генерировать
корректные значения нулевых указателей в ответ на 0, встретившийся в
соответствующем контексте
1.5 Пусть NULL был определен следующим образом:
#define NULL ((char *)0).
Ознает ли это, что функциям можно передавать NULL без преобразования
типа?
О: В общем, нет. Проблема в том, что существуют компьютеры, которые
используют различные внутренние представления для указателей на
различные типы данных. Предложенное определение через #define
годится, когда функция ожидает в качестве передаваемого параметра
указатель на char, но могут возникнуть проблемы при передаче
указателей на переменные других типов, а верная конструкция
FILE *fp = NULL;
может не сработать.
Тем не менее, ANSI C допускает другое определение для NULL:
#define NULL ((void *)0)
Кроме помощи в работе некорректным программам (но только в случае
машин, где указатели на разные типы имеют одинаковые размеры, так
что помощь здесь сомнительна) это определение может выявить
программы, которые неверно используют NULL (например, когда был
необходим символ ASCII NUL; см. вопрос 1.8).
Смотри: ANSI Rationale Разд. 4.1.5 c. 74.
1.6: Я использую макрос
#define Nullptr(type) (type *)0 ,
который помогает задавать тип нулевого указателя.
О: Хотя этот трюк и популярен в определенных кругах, он стоит немного.
Он не нужен при сравнении и присваивании; см. вопрос 1.2. Он даже
не экономит буквы. Его использование показывает тому, кто читает
программу, что автор здорово "сечет" в нулевых указателях, и требует
гораздо более аккуратной проверки определения макроса, его
использования и _всех_ остальных случаев применения указателей.
См. также вопрос 8.1.
1.7: Корректно ли использовать сокращенный условный оператор if(p) для
проверки того, что указатель ненулевой? А что если внутреннее
представление для нулевых указателей отлично от нуля?
О: Когда С требует логическое значение выражения (в инструкциях if,
while, for, и do и для операторов &&, ||, !, и ?:) значение false
получается, когда выражение равно нулю, а значение true получается в
противоположном случае. Таким образом, если написано
if(expr)
где "expr" - произвольное выражение, компилятор на самом деле
поступает так, как будто было написано
if(expr != 0)
Подставляя тривиальное выражение, содержащее указатель "p" вместо
"expr", получим
if(p) эквивалентно if(p != 0)
и это случай, когда происходит сравнение, так что компилятор поймет,
что неявный ноль - это нулевой указатель и будет использовать
правильное значение. Здесь нет никакого подвоха, компиляторы работают
именно так и генерируют в обоих случаях идентичный код. Внутреннее
представление указателя _не_ имеет значения.
Оператор логического отрицания ! может быть описан так:
!expr на самом деле эквивалентно expr?0:1
Читателю предлагается в качестве упражнения показать, что
if(!p) эквивалентно if(p == 0)
Хотя "сокращения" типа if(p) совершенно корректны, кое-кто считает
их использование дурным стилем.
См. также вопрос 8.2.
Смотри: K&R II Разд. A7.4.7 c. 204; H&S Разд. 5.3 c. 91; ANSI
Разд. 3.3.3.3, 3.3.9, 3.3.13, 3.3.14, 3.3.15, 3.6.4.1, и
3.6.5 .
1.8: Если "NULL" и "0" эквивалентны, то какую форму из двух использовать?
О: Многие программисты верят, что "NULL" должен использоваться во всех
выражениях, содержащих указатели как напоминание о том, что значение
должно рассматриваться как указатель. Другие же чувствуют, что
путаница, окружающая "NULL" и "0", только усугубляется, если "0"
спрятать в операторе #define и предпочитают использовать "0" вместо
"NULL". Единственного ответа не существует. Программисты на С должны
понимать, что "NULL" и "0" взаимозаменяемы и что "0" без
преобразования типа можно без сомнения использовать при
инициализации, присваивании и сравнении. Любое использование "NULL"
(в противоположность "0" ) должно рассматриваться как ненавязчивое
напоминание, что используется указатель; программистам не нужно
ничего делать (как для своего собственного понимания, так и для
компилятора) для того, чтобы отличать нулевые указатели от целого
числа 0. NULL _нельзя_ использовать, когда необходим другой тип нуля.
Даже если это и будет работать, с точки зрения стиля
программирования это плохо.(ANSI позволяет определить NULL с помощью
#define как (void *)0. Такое определение не позволит использовать
NULL там, где не подразумеваются указатели). Особенно не рекомендуется
использовать NULL там, где требуется нулевой код ASCII (NUL). Если
необходимо, напишите собственное определение
#define NUL '\0'
Смотри: K&R II Разд. 5.4 c. 102.
1.9 Но не лучше ли будет использовать NULL (вместо 0) в случае, когда
значение NULL изменяется, быть может, на компьютере с ненулевым
внутренним представлением нулевых указателей?
О: Нет. Хотя символические константы часто используются вместо чисел
из-за того, что числа могут измениться, в данном случае причина, по
которой используется NULL, иная. Еще раз повторим: язык гарантирует,
что 0, встреченный там, где по контексту подразумевается указатель,
будет заменен компилятором на нулевой указатель. NULL используется
только с точки зрения лучшего стиля программирования.
1.10: Я в растерянности. Гарантируется, что NULL равен 0, а нулевой
указатель нет?
О: Термин "null" или "NULL" может не совсем обдуманно использоваться
в нескольких смыслах:
1. Нулевой указатель как абстрактное понятие языка, определенное
в вопросе 1.1. Он представляется с помощью...
2. Внутреннее (на стадии выполнения) представление нулевого
указателя, которое может быть отлично от нуля и различаться
для различных типов указателей. О внутреннем представлении
нулевого указателя должны заботиться только создатели
компилятора. Программистам на С это представление не
известно, поскольку они используют...
3. Синтаксическое соглашение для нулевых указателей, символ
"0". Вместо него часто используют...
4. Макрос NULL который с помощью #define определен как "0"
или "(void *)0". Наконец, нас может запутать...
5. Нулевой код ASCII (NUL), в котором все биты равны нулю, но
который имеет мало общего с нулевым указателем, разве что
названия похожи; и...
6. "Нулевой стринг", или, что то же самое, пустой стринг ("").
Термин "нулевой стринг" может приводить к путанице в С и,
возможно, его следует избегать, так как пустой стринг
включает символ '\0', но не нулевой указатель, и здесь мы
уже идем по кругу...
В этом документе фраза "нулевой указатель" (прописными буквами)
используется в смысле 1, символ "0" в смысле 3, а слово "NULL",
записанное большими буквами, в смысле 4.
1.11: Почему так много путаницы связано с нулевыми указателями? Почему
так часто возникают вопросы?
О: Программисты на С традиционно хотят знать больше, чем это необходимо
для программирования, о внутреннем представлении кода. Тот факт,
что внутреннее представление нулевых указателей для большинства
машин совпадает с их представлением в исходном тексте, т.е. нулем,
способствует появлению неверных обобщений. Использование макроса
(NULL) предполагает, что значение может впоследствии измениться, или
иметь другое значение для какого-нибудь компьютера. Конструкция
"if(p == 0)" может быть истолкована неверно, как преобразование перед
сравнением p к целому типу, а не 0 к типу указателя. Наконец, часто
не замечают, что термин "null" употребляется в разных смыслах
(перечисленных выше).
Хороший способ устранить путаницу - вообразить, что язык С имеет
ключевое слово (возможно, nil, как в Паскале), которое обозначает
нулевой указатель. Компилятор либо пребразует "nil" в нулевой
указатель нужного типа, либо сообщает об ошибке, когда этого сделать
нельзя. На самом деле, ключевое слово для нулевого указателя в С
- это не "nil" а "0". Это ключевое слово работает всегда, за
исключением случая, когда компилятор воспринимает в неподходящем
контексте "0" без указания типа как целое число, равное нулю, вместо
того, чтобы сообщить об ошибке. Программа может не работать, если
предполагалось, что "0" без явного указания типа - это нулевой
указатель.
1.12: Я все еще в замешательстве. Мне так и не понятна возня с нулевыми
указателями.
О: Следуйте двум простым правилам:
1. Для обозначения в исходном тексте нулевого указателя,
используйте "0" или "NULL".
2. Если "0" или "NULL" используются как фактические
аргументы при вызове функции, приведите их к типу указателя,
который ожидает вызываемая функция.
Остальная часть дискуссии посвящена другим заблуждениям, связанным с
нулевыми указателями, внутреннему представлению нулевых указателей
(которое Вам знать не обязательно), а также усовершенствованиям
стандарта ANSI C. Изучите ответы на вопросы 1.1, 1.2, и 1.3, а
также учтите вопросы 1.8 и 1.11, и все будет нормально.
1.13: Учитывая всю эту путаницу, связанную с нулевыми указателями, не лучше
ли просто потребовать, чтобы их внутреннее представление было нулевым?
О: Если причина только в этом, то поступать так было бы неразумно, так
как это неоправданно ограничит конкретную реализацию, которая (без
таких ограничений) будет естественным образом представлять нулевые
указатели специальными, отличными от нуля значениями, особенно когда
эти значения автоматически будут вызывать специальные аппаратные
прерывания, связанные с неверным доступом.
Кроме того, что это требование даст на практике? Понимание нулевых
указателей не требует знаний о том, нулевое или ненулевое их
внутреннее представление. Предположение о том, что внутреннее
представление нулевое, не приводит к упрощению кода (за исключением
некоторых случаем сомнительного использования calloc; см. вопрос
3.13). Знание того, что внутреннее представление равно нулю, не
упростит вызовы функций, так как _размер_ указателя может быть
отличным от размера указателя на int. (Если вместо "0" для
обозначения нулевого указателя использовать "nil" (см. вопрос 1.11),
необходимость в нулевом внутреннем представлении нулевых указателей
даже бы не возникла).
1.14: Ну а если честно, на какой-нибудь реальной машине используются
ненулевые внутренние представления нулевых указателей или разные
представления для указателей разных типов?
О: Серия Prime 50 использует сегмент 07777, смещение 0 для нулевого
указателя, по крайней мере, для PL/I. Более поздние модели используют
сегмент 0, смещение 0 для нулевых указателей С, что делает
необходимыми новые инструкции, такие как TCNP (проверить нулевой
указатель С), которые вводятся для совместимости с уцелевшими
скверно написанными С программами, основанными на неверных
предположениях. Старые машины Prime с адресацией слов были печально
знамениты тем, что указатели на байты (char *) у них были большего
размера, чем указатели на слова (int *).
Серия Eclipse MV корпорации Data General имеет три аппаратно
поддерживаемых типа указателей (указатели на слово, байт и бит), два
из которых - char * и void * используются компиляторами С. Указатель
word * используется во всех других случаях.
Некоторые центральные процессоры Honeywell-Bull используют код 06000
для внутреннего представления нулевых указателей.
Серия CDC Cyber 180 использует 48-битные указатели, состоящие из
кольца (ring), сегмента и смещения. Большинство пользователей
(в кольце 11) имеют в качестве нулевых указателей код 0xB00000000000.
Символическая Лисп-машина с теговой архитектурой даже не имеет
общеупотребительных указателей; она использует пару <NIL,0> (вообще
говоря, несуществующий <объект, смещение> хендл) как нулевой
указатель С.
В зависимости от модели памяти, процессоры 80*86 (PC) могут
использовать либо 16-битные указатели на данные и 32-битные указатели
на функции, либо, наоборот, 32-битные указатели на данные и 16-битные
- на функции.
Старые модели HP 3000 используют различные схемы адресации для байтов
и для слов. Указатели на char и на void, имеют, следовательно,
другое представление, чем указатели на int (на структуры и т.п.),
даже если адрес одинаков.
1.15 Что означает ошибка во время исполнения "null pointer assignment"
(запись по нулевому адресу). Как мне ее отследить?
О: Это сообщение появляется только в системе MS-DOS (см., следовательно,
раздел 16) и означает, что произошла запись либо с помощью
неинициализированного, либо нулевого указателя в нулевую область.
Отладчик обычно позволяет установить точку останова при доступе к
нулевой области. Если это сделать нельзя, Вы можете скопировать около
20 байт из области 0 в другую и периодически проверять, не изменились
ли эти данные.
2. Указатели и массивы
2.1: В одном файле у меня есть описание char a[6] а в другом я объявил
extern char *a. Почему это не работает?
О: Декларация extern char *a просто не совпадает с текущим определением.
Тип "Указатель-на-тип-Т" не равен типу "массив-типа-Т". Используйте
extern char a[].
2.2: Но я слышал, что char a[] эквивалентно char *a.
О: Ничего подобного. (То, что Вы слышали, касается формальных параметров
функций, см. вопрос 2.4.) Массивы - не указатели. Объявление массива
"char a[6];" требует определенного места для шести символов, которое
будет известно под именем "a". То есть, существует место под именем
"a", в которое могут быть помещены 6 символов. С другой стороны,
объявление указателя "char *p;" требует места только для самого
указателя. Указатель будет известен под именем "p" и может указывать
на любой символ (или непрерывный массив символов).
Как обычно, лучше один раз увидеть, чем сто раз услышать.
Объявление
char a[] = "hello";
char *p = "world";
породит структуры данных, которые могут быть представлены так:
+---+---+---+---+---+---+
a: | h | e | l | l | o |\0 |
+---+---+---+---+---+---+
+-----+ +---+---+---+---+---+---+
p: | *======> | w | o | r | l | d |\0 |
+-----+ +---+---+---+---+---+---+
Важно понимать, что ссылка типа х[3] порождает разный код в
зависимости от того, массив х или указатель. Если взять приведенную
выше декларацию, то, когда компилятор встречается с выражением а[3],
он генерирует код, позволяющий переместиться к месту под именем "a",
перемещается на три символа вперед и затем читает требуемый символ.
В случае выражения p[3] компилятор генерирует код, чтобы начать с
позиции "p", считывает значение указателя, прибавляет к указателю
3 и, наконец, читает символ, на который указывает указатель. В нашем
примере и a[3] и p[3] оказались равны 'l', но компилятор получает
этот символ по-разному. (Смотри также вопросы 17.19 и 17.20.)
2.3: Тогда что же понимается под "эквивалентностью указателей и массивов"
в С?
О: Большая часть путаницы вокруг указателей в С происходит от
непонимания этого утверждения. "Эквивалентность" указателей и
массивов не позволяет говорить не только об идентичности, но и
взаимозаменяемости.
"Эквивалентность" относится к следующему ключевому определению:
значение [см. вопрос 2.5] типа массив-Т, которое появляется
в выражении, превращается (за исключением трех случаев) в
указатель на первый элемент массива; тип результирующего
указателя - указатель-на-Т.
(Исключение составляют случаи, когда массив оказывается операндом
sizeof, оператора & или инициализатором символьной строки для
массива литер.)
Вследствие этого определения нет заметной разницы в поведении
оператора индексирования [], если его применять к массивам и
указателям. Согласно правилу, приведенному выше, в выражении типа
а[i] ссылка на массив "a" превращается в указатель и дальнейшая
индексация происходит так, как будто существует выражение с
указателем p[i] (хотя доступ к памяти будет различным, как описано в
ответе на вопрос 2.2). В любом случае выражение x[i], где х - массив
или указатель) равно по определению *((x)+(i)).
Смотри: K&R I Разд.5.3 c.93-6; K&R II Разд.5.3 c. 99; H&S
Разд.5.4.1 c. 93; ANSI Разд.3.2.2.1, Разд.3.3.2.1,
Разд.3.3.6 .
2.4: Тогда почему объявления указателей и массивов взаимозаменяемы в
в качестве формальных параметров?
О: Так как массивы немедленно превращаются в указатели, массив на самом
деле не передается в функцию. По общему правилу, любое похожее на
массив объявление параметра
f(a)
char a[];
рассматривается компилятором как указатель, так что если был передан
массив, функция получит:
f(a)
char *a;
Это превращение происходит только для формальных параметров функций,
больше нигде. Если это превращение раздражает Вас, избегайте его;
многие пришли к выводу, что порождаемая этим путаница перевешивает
небольшое преимущество от того, что объявления смотрятся как вызов
функции и/или напоминают о том, как параметр будет использоваться
внутри функции.
Смотри: K&R I Разд.5.3 c. 95, Разд.A10.1 c. 205; K&R II
Разд.5.3 c. 100, Разд.A8.6.3 c. 218, Разд.A10.1 c.226; H&S
Разд.5.4.3 c. 96; ANSI Разд.3.5.4.3, Разд.3.7.1, CT&P Разд.3.3
c. 33-4.
2.5 Как массив может быть значением типа lvalue, если нельзя присвоить
ему значение?
О: Стандарт ANSI C определяет "модифицируемое lvalue", но массив к
этому не относится.
Смотри: ANSI Разд. 3.2.2.1 c. 37.
2.6 Почему sizeof неправильно определяет размер массива, который передан
функции в качестве параметра?
О: Оператор sizeof сообщает размер указателя, который на самом деле
получает функция. (см. вопрос 2.4).
2.7 Кто-то объяснил мне, что массивы это на самом деле только постоянные
указатели.
О: Это слишком большое упрощение. Имя массива - это константа,
следовательно, ему нельзя присвоить значение, но массив - это не
указатель, как должно быть ясно из ответа на вопрос 2.2 и из
картинки, помещенной там же.
2.8 C практической точки зрения в чем разница между массивами и
указателями?
О: Массивы автоматически резервируют память, но не могут изменить
расположение в памяти и размер. Указатель должен быть задан так,
чтобы явно указывать на выбранный участок памяти (возможно с помощью
malloc), но он может быть по нашему желанию переопределен (т.е. будет
указывать на другие объекты) и, кроме того, указатель имеет много
других применений, кроме службы в качестве базового адреса блоков
памяти.
В рамках так называемой эквивалентности массивов и указателей (см.
вопрос 2.3), массивы и указатели часто оказываются взаимозаменяемыми.
Особенно это касается блока памяти, выделенного функцией malloc,
указатель на который часто используется как настоящий массив. (На
этот блок памяти можно ссылаться, используя [], cм. вопрос 2.14,
а также вопрос 17.20.)
2.9 Я наткнулась на шуточный код, содержащий "выражение" 5["abcdef"].
Почему такие выражения возможны в С?
О: Да, Вирджиния, индекс и имя массива можно переставлять в С. Этот
забавный факт следует из определения индексации через указатель, а
именно, a[e] идентично *((a)+(e)), для _любого_ выражения е и
основного выражения а, до тех пор пока одно из них будет указателем,
а другое целочисленным выражением. Это неожиданная коммутативность
часто со странной гордостью упоминается в С-текстах, но за пределами
Соревнований по Непонятному Программированию (Obfuscated C Contest)
она применения не находит. (см. вопрос 17.13).
Смотри: ANSI Rationale Разд. 3.3.2.1 c. 41.
2.10 Мой компилятор ругается, когда я передаю двумерный массив функции,
ожидающей указатель на указатель.
О: Правило, по которому массивы превращаются в указатели не может
применяться рекурсивно. Массив массивов (т.е. двумерный массив в С)
превращается в указатель на массив, а не в указатель на указатель.
Указатели на массивы могут вводить в заблуждение и применять их нужно
с осторожностью. (Путаница еще более усугубляется тем, что существуют
некорректные компиляторы, включая некоторые версии pcc и полученные
на основе pcc программы lint, которые неверно вопринимают присваивание
многоуровневым указателям многомерных массивов.) Если вы передаете
двумерный массив функции:
int array[NROWS][NCOLUMNS];
f(array);
описание функции должно соответствовать
f(int a[][NCOLUMNS]) {...}
или
f(int (*ap)[NCOLUMNS]) {...} /* ap - указатель на массив */
В случае, когда используется первое описание, компилятор неявно
осуществляет обычное преобразование "массива массивов" в "указатель
на массив"; во втором случае указатель на массив задается явно.
Так как вызываемая функция не выделяет место для массива, нет
необходимости знать его размер, так что количество "строк" NROWS
может быть опущено. "Форма" массива по-прежнему важна, так что
размер "столбца" NCOLUMNS должен быть включен (а для массивов
размерности 3 и больше, все промежуточные размеры).
Если формальный параметр функции описан как указатель на указатель,
то передача функции в качестве параметра двумерного массива будет,
видимо, некорректной.
Смотри: K&R I Разд.5.10 c. 110; K&R II Разд.5.9 c. 113.
2.11: Как писать функции, принимающие в качестве параметра двумерные
массивы, "ширина" которых во время компиляции неизвестна?
О: Это непросто. Один из путей - передать указатель на элемент [0][0]
вместе с размерами и затем симулировать индексацию "вручную":
f2(aryp, nrows, ncolumns)
int *aryp;
int nrows, ncolumns;
{ ... array[i][j] это aryp[i * ncolumns + j] ... }
Этой функции массив из вопроса 2.10 может быть передан так:
f2(&array[0][0], NROWS, NCOLUMNS);
Нужно, однако, заметить, что программа, выполняющая индексирование
многомерного массива "вручную" не полностью соответствует стандарту
ANSI C; поведение (&array[0][0])[x] не определено при x> NCOLUMNS.
gcc разрешает объявлять локальные массивы, которые имеют размеры,
задаваемые аргументами функции, но это - нестандартное расширение.
См. также вопрос 2.15.
2.12: Как объявить указатель на массив?
О: Обычно этого делать не нужно. Когда случайно говорят об указателе на
массив, обычно имеют в виду указатель на первый элемент массива.
Вместо указателя на массив рассмотрим использование указателя на один
из элементов массива. Массивы типа T превращаются в указатели типа Т
(см. вопрос 2.3), что удобно; индексация или увеличение указателя
позволяет иметь доступ к отдельным элементам массива. Истинные
указатели на массивы при увеличении или индексации указывают на
следующий массив и в общем случае если и полезны, то лишь при
операциях с массивами массивов. (Cм. вопрос 2.10 выше.)
Если действительно нужно объявить указатель на целый массив,
используйте что-то вроде "int (*ap)[N];" где N - размер массива.
(Cм. также вопрос 10.4.) Если размер массива неизвестен, параметр
N может быть опущен, но получившийся в результате тип " указатель
на массив неизвестного размера" - бесполезен.
2.13 Исходя из того, что ссылки на массив превращаются в указатели,
скажите в чем разница для массива
int array[NROWS][NCOLUMNS];
между array и &array?
O: Согласно ANSI/ISO стандарту C, &array дает указатель типа
"указатель-на-массив-Т", на весь массив (Cм. также вопрос 2.12).
В языке C до выхода стандарта ANSI оператор & в &array игнорировался,
порождая предупреждение компилятора. Все компиляторы C, встречая
просто имя массива, порождают указатель типа указатель-на-Т, т.е. на
первый элемент массива. (Cм. также вопрос 2.3.)
2.14: Как динамически выделить память для многомерного массива?
О: Лучше всего выделить память для массива указателей, а затем
инициализировать каждый указатель так, чтобы он указывал на
динамически создаваемую строку. Вот пример для двумерного массива:
int **array1 = (int **)malloc(nrows * sizeof(int *));
for(i = 0; i < nrows; i++)
array1[i] = (int *)malloc(ncolumns * sizeof(int));
(В "реальной" программе, malloc должна быть правильно объявлена, а
каждое возвращаемое malloc значение - проверено.)
Можно поддерживать монолитность массива, (одновременно затрудняя
последующий перенос в другое место памяти отдельных строк), с помощью
явно заданных арифметических действий с указателями:
int **array2 = (int **)malloc(nrows * sizeof(int *));
array2[0] = (int *)malloc(nrows * ncolumns * sizeof(int));
for(i = 1; i < nrows; i++)
array2[i] = array2[0] + i * ncolumns;
В любом случае доступ к элементам динамически задаваемого массива
может быть произведен с помощью обычной индексации: array[i][j].
Если двойная косвенная адресация, присутствующая в приведенных выше
примерах, Вас по каким-то причинам не устраивает, можно имитировать
двумерный массив с помощью динамически задаваемого одномерного
массива:
int *array3 = (int *)malloc(nrows * ncolumns * sizeof(int));
Теперь, однако, операции индексирования нужно выполнять вручную,
осуществляя доступ к элементу i,j с помощью array3[i*ncolumns+j].
(Реальные вычисления можно скрыть в макросе, однако вызов макроса
требует круглых скобок и запятых, которые не выглядят в точности
так, как индексы многомерного массива.)
Наконец, можно использовать указатели на массивы:
int (*array4)[NCOLUMNS] =
(int(*)[NCOLUMNS])malloc(nrows * sizeof(*array4));,
но синтакс становится устрашающим, и "всего лишь" одно измерение
должно быть известно во время компиляции.
Пользуясь описанными приемами, необходимо освобождать память,
занимаемую массивами (это может проходить в несколько шагов; см.
вопрос 3.9), когда они больше не нужны, и не следует смешивать
динамически создаваемые массивы с обычными, статическими (cм. вопрос
2.15 ниже, а также вопрос 2.10).
2.15 Как мне равноправно использовать статически и динамически задаваемые
многомерные массивы при передаче их в качестве параметров функциям?
О: Идеального решения не существует. Возьмем объявления
int array[NROWS][NCOLUMNS];
int **array1;
int **array2;
int *array3;
int (*array4)[NCOLUMNS];
соответствующие способам выделения памяти в вопросах 2.10 и 2.14,
и функции, объявленные как
f1(int a[][NCOLUMNS], int m, int n);
f2(int *aryp, int nrows, int ncolumns);
f3(int **pp, int m, int n);
(см. вопросы 2.10 и 2.11). Тогда следующие вызовы должны работать
так, как ожидается
f1(array, NROWS, NCOLUMNS);
f1(array4, nrows, NCOLUMNS);
f2(&array[0][0], NROWS, NCOLUMNS);
f2(*array2, nrows, ncolumns);
f2(array3, nrows, ncolumns);
f2(*array4, nrows, NCOLUMNS);
f3(array1, nrows, ncolumns);
f3(array2, nrows, ncolumns);
Следующие два вызова, возможно, будут работать, но они включают
сомнительные приведения типов, и работают лишь в том случае, когда
динамически задаваемое число столбцов ncolumns совпадает с NCOLUMS:
f1((int (*)[NCOLUMNS])(*array2), nrows, ncolumns);
f1((int (*)[NCOLUMNS])array3, nrows, ncolumns);
Необходимо еще раз отметить, что передача &array[0][0] функции f2
не совсем соответствует стандарту; см. вопрос 2.11.
Если Вы способны понять, почему все вышеперечисленные вызовы
работают и написаны именно так, а не иначе, и если Вы понимаете,
почему сочетания, не попавшие в список, работать не будут, то у Вас
_очень_ хорошее понимание массивов и указателей (и нескольких других
областей) C.
2.16 Вот изящный трюк: если я пишу
int realarray[10];
int *array = &realarray[-1];,
то теперь можно рассматривать "array" как массив, у которого индекс
первого элемента равен единице.
О: Хотя этот прием внешне привлекателен (и использовался в старых
изданиях книги "Numerical Recipes in С"), он не удовлетворяет
стандартам С. Арифметические действия над указателями определены лишь
тогда, когда указатель ссылается на выделенный блок памяти или на
воображаемый завершающий элемент, следующий сразу за блоком. В
противном случае поведение программы не определено, _даже если
указатель не переназначается_. Код, приведенный выше, плох тем,
что при уменьшении смещения может быть получен неверный адрес
(возможно, из-за циклического перехода адреса при пересечении
границы сегмента).
Смотри: ANSI Разд.3.3.6 c. 48, Rationale Разд.3.2.2.3 c. 38;
K&R II Разд.5.3 c. 100, Разд.5.4 c. 102-3, Разд.A7.7 c. 205-6.
2.17: Я передаю функции указатель, который она инициализирует
...
int *ip;
f(ip);
...
void f(ip)
int *ip;
{
static int dummy = 5;
ip = &dummy;
}
но указатель после вызова функции остается неизменным.
О: Функция пытается изменить сам указатель, или то, на что он ссылается?
Помните, что аргументы в С передаются по значению. Вызываемая функция
изменяет только копию передаваемого указателя. Вам нужно либо передать
адрес указателя (функцию будет в этом случае принимать указатель
на указатель), либо сделать так, чтобы функция возвращала указатель.
2.18 У меня определен указатель на char, который указывает еще и на int,
причем мне необходимо переходить к следующему элементу типа int.
Почему
((int *)p)++;
не работает?
О: В языке С оператор преобразования типа не означает "будем действовать
так, как будто эти биты имеют другой тип"; это оператор, который
действительно выполняет преобразования, причем по определению
получается значение типа rvalue, которому нельзя присвоить новое
значение и к которому не применим оператор ++. (Следует считать
аномалией то, что компиляторы pcc и расширения gcc вообще
воспринимают выражения приведенного выше типа.).
Скажите то, что думаете:
p = (char *)((int *)p + 1);
или просто
p += sizeof(int);
Смотри: ANSI Разд.3.3.4, Rationale Разд.3.3.2.4 c. 43.
2.19 Могу я использовать void ** , чтобы передать функции по ссылке
обобщенный указатель?
О: Стандартного решения не существует, поскольку в С нет общего типа
указатель-на-указатель. void * выступает в роли обобщенного
указателя только потому, что автоматически осуществляются
преобразования в ту и другую сторону, когда встречаются разные типы
указателей. Эти преобразования не могут быть выполнены (истинный
тип указателя неизвестен), если осуществляется попытка косвенной
адресации, когда void ** указывает на что-то отличное от void *.
3. Выделение памяти
3.1: Почему не работает фрагмент кода?
char *answer;
printf("Type something:\n");
gets(answer);
printf("You typed \"%s\"\n", answer);
О: Указатель "answer", который передается функции gets как место, в
котором должны храниться вводимые символы, не инициализирован, т.е.
не указывает на какое-то выделенное место. Иными словами, нельзя
сказать, на что указывает "answer". (Так как локальные переменные не
инициализируются, они вначале обычно содержат "мусор", то есть даже
не гарантируется, что в начале "answer" - это нулевой указатель.
См. вопрос 17.1).
Простейший способ исправить программу - использовать локальный массив
вместо указателя, предоставив компилятору заботу о выделении памяти:
#include <string.h>
char answer[100], *p;
printf("Type something:\n");
fgets(answer, sizeof(answer), stdin);
if((p = strchr(answer, '\n')) != NULL)
*p = '\0';
printf("You typed \"%s\"\n", answer);
Заметьте, что в этом примере используется fgets() вместо gets()
(это признак хорошего тона, см. вопрос 11.6), что позволяет указать
размер массива, так что выход за пределы массива, когда пользователь
введет слишком длинную строку, становится невозможным. (К сожалению,
fgets() не удаляет автоматически завершающий символ конца строки \n,
как это делает gets()). Для выделения памяти можно также
использовать malloc().
3.2: Не могу заставить работать strcat. В моей программе
char *s1 = "Hello, ";
char *s2 = "world!";
char *s3 = strcat(s1, s2);
но результаты весьма странные.
О: Проблема снова состоит в том, что не выделено место для результата
объединения. С не поддерживает автоматически переменные типа string.
Компиляторы С выделяют память только под объекты, явно указанные
в исходном тексте (в случае стрингов это может быть массив литер или
символы, заключенные в двойные кавычки). Программист должен сам
позаботиться о том, чтобы была выделена память для результата,
который получается в процессе выполнения программы, например
результата объединения строк. Обычно это достигается объявлением
массива или вызовом malloc. (см. также вопрос 17.20).
Функция strcat не выделяет память; вторая строка присоединяется к
первой. Следовательно, одно из исправлений - в задании первой строки
в виде массива достаточной длины
char s1[20] = "Hello, ";
Так как strcat возвращает указатель на первую строку (в нашем случае
s1), переменная s3 - лишняя.
Смотри: CT&P Разд. 3.2 c. 32.
3.3 Но в справочнике о функции strcat сказано, что она использует в
качестве аргументов два указателя на char. Откуда мне знать о
выделении памяти?
О: Как правило, при использовании указателей _всегда_ необходимо
иметь в виду выделение памяти, по крайней мере, быть уверенным,
что компилятор делает это для Вас. Если в документации на
библиотечную функцию явно ничего не сказано о выделении памяти,
то обычно это проблема вызывающей функции.
Краткое описание функции в верхней части страницы справочника в
стиле UNIX может ввести в заблуждение. Приведенные там фрагменты
кода ближе к определению, необходимому для разработчика функции,
чем для того, кто будет эту функцию вызывать. В частности, многие
функции, имеющие в качестве параметров указатели (на структуры или
стринги, например), обычно вызываются с параметрами, равными адресам
каких-то уже существующих объектов( структур или массивов - см.
вопросы 2.3, 2.4.) Другой распространенный пример - функция stat().
3.4 Предполагается, что функция, которую я использую, возвращает строку,
но после возврата в вызывающую функцию эта строка содержит "мусор".
О: Убедитесь, что правильно выделена область памяти, указатель на
которую возвращает Ваша функция. Функция должна возвращать указатель
на статически выделеннную область памяти или на буфер, передаваемый
функции в качестве параметра, или на память, выделенную с помощью
malloc(), но _не_ на локальный (auto) массив. Другими словами,
никогда никогда не делайте ничего похожего на
char *f()
{
char buf[10];
/* ... */
return buf;
}
Приведем одну поправку (непригодную в случае, когда f() вызывается
рекурсивно, или когда одновременно нужны несколько возвращаемых
значений)
static char buf[10];
См. также вопрос 17.5.
3.5 Почему в некоторых исходных текстах значения, возвращаемые malloc(),
аккуратно преобразуются в указатели на выделяемый тип памяти?
О: До того как стандарт ANSI/ISO ввел обобщенный тип указателя void *,
эти преобразования были обычно необходимы для подавления
предупреждений компилятора о приравнивании указателей разных типов.
(Согласно стандарту C ANSI/ISO, такие преобразования типов
указателей не требуются).
3.6 Можно использовать содержимое динамически выделяемой памяти после
того как она освобождена?
О: Нет. Иногда в старых описаниях malloc() говорилось, что содержимое
освобожденной памяти "остается неизменным"; такого рода поспешная
гарантия никогда не была универсальной и не требуется стандартом ANSI.
Немногие программисты стали бы нарочно использовать содержимое
освобожденной памяти, но это легко сделать нечаянно. Рассмотрите
следующий (корректный) фрагмент программы, в котором освобождается
память, занятая односвязным списком:
struct list *listp, *nextp;
for(listp = base; listp != NULL; listp = nextp) {
nextp = listp->next;
free((char *)listp);
}
и подумайте, что получится, если будет использовано на первый взгляд
более очевидное выражение для тела цикла listp = listp->next, без
временного указателя nextp.
См.: ANSI Rationale Разд. 4.10.3.2 c. 102; CT&P Разд. 7.10 c. 95.
3.7 Откуда free() знает, сколько байт освобождать?
О: Функции malloc/free запоминают размер каждого выделяемого и
возвращаемого блока, так что не нужно напоминать размер
освобождаемого блока.
3.8 А могу я узнать действительный размер выделяемого блока?
О: Нет универсального ответа.
3.9 Я выделяю память для структур, которые содержат указатели на
другие динамически создаваемые объекты. Когда я освобождаю память,
занятую структурой, должен ли я сначала осводить память, занятую
подчиненным объектом?
О: Да. В общем, необходимо сделать так, чтобы каждый указатель,
возвращаемый malloc() был передан free() точно один раз (если память
освобождается).
3.10 В моей программе сначала с помощью malloc() выделяется память, а
затем большое количество памяти освобождается с помощью free(), но
количество занятой памяти (так сообщает команда операционной системы)
не уменьшается.
О: Большинство реализаций malloc/free не возвращают освобожденную
память операционной системе (если таковая имеется), а просто
делают освобожденную память доступной для будущих вызовов malloc() в
рамках того же процесса.
3.11 Должен ли я освобождать выделенную память перед возвратом в
операцинную систему?
О: Делать это не обязательно. Настоящая операционная система
восстанавливает состояние памяти по окончании работы программы.
Тем не менее, о некоторых персональных компьютерах известно, что они
ненадежны при восстановлении памяти, а из стандарта ANSI/ISO
можно лишь получить указание, что эти вопросы относятся к "качеству
реализации".
См. ANSI Разд. 4.10.3.2 .
3.12 Правильно ли использовать нулевой указатель в качестве первого
аргумента функции realloc()? Зачем это нужно?
О: Это разрешено стандартом ANSI C (можно также использовать
realloc(...,0) для освобождения памяти), но некоторые ранние
реализации С это не поддерживают, и мобильность в этом случае не
гарантируется. Передача нулевого указателя realloc() может упростить
написание самостартующего алгоритма пошагового выделения памяти.
См. ANSI Разд. 4.10.3.4 .
3.13: В чем разница между calloc и malloc? Получатся ли в результате
примения calloc корректные значения нулевых указателей и чисел с
плавающей точкой? Освобождает ли free память, выделенную calloc,
или нужно использовать cfree?
О: По существу calloc(m,n) эквивалентна
p = malloc(m * n);
memset(p, 0, m * n);
Заполнение нулями означает зануление всех битов, и, следовательно,
не гарантирует нулевых значений для указателей (см. раздел 1) и
для чисел с плавающей точкой. Функция free может (и должна)
использоваться для освобождения памяти, выделенной calloc.
Смотри: ANSI Разделы от 4.10.3 до 4.10.3.2 .
3.14: Что такое alloca и почему использование этой функции обескураживает?
О: alloca выделяет память, которая автоматически освобождается, когда
происходит возврат из функции, в которой вызывалась alloca. То есть,
память, выделенная alloca, локальна по отношению к "стековому кадру"
или контексту данной функции.
Использование alloca не может быть мобильным, реализации этой
функции трудны на машинах без стека. Использование этой функции
проблематично (и очевидная реализация на машинах со стеком не
удается), когда возвращаемое ей значение непосредственно передается
другой функции, как, например, в fgets(alloca(100), 100, stdin).
По изложенным выше причинам alloca (вне зависимости от того,
насколько это может быть полезно) нельзя использовать в программах,
которые должны быть в высокой степени мобильны.
См. ANSI Rationale Разд. 4.10.3 c. 102.
4. Выражения
4.1: Почему вот такой код
a[i] = i++;
не работает?
О: Подвыражение i++ приводит к побочному эффекту - значение i
изменяется, что приводит к неопределенности, если i уже встречается в
том же выражении. (Обратите внимание на то, что хотя в книге K&R
говорится, что поведение подобных выражений не описано, стандарт
ANSI/ISO утверждает, что поведение не определено - см. вопрос 5.23.)
См. ANSI Разд.3.3 c. 39.
4.2: Пропустив код
int i = 7;
printf("%d\n", i++ * i++);
через свой компилятор, я получил на выходе 49. А разве, независимо
от порядка вычислений, результат не должен быть равен 56?
О: Хотя при использовании постфиксной формы операторов ++ и --
увеличение и уменьшение выполняется после того как первоначальное
значение использовано, тайный смысл слова "после" часто понимается
неверно. _Не_ гарантируется, что увеличение или уменьшение будет
выполнено немедленно после использования первоначального значения
перед тем как будет вычислена любая другая часть выражения. Просто
гарантируется, что измение будет произведено в какой-то момент до
окончания вычисления (перед следующей "точкой последовательности"
в терминах ANSI C). В приведенном примере компилятор умножил
предыдущее значение само на себя и затем дважды увеличил i на 1.
Поведение кода, содержащего многочисленные двусмысленные побочные
эффекты неопределено (см. вопрос 5.23). Даже не пытайтесь выяснить,
как Ваш компилятор все это делает (в противоположность неумным
упражнениям во многих книгах по С); в K&R мудро сказано: "Да
хранит Вас Ваша невинность, если Вы не знаете, как это делается
на разных машинах"
См.: K&R I Разд. 2.12 c. 50; K&R II Разд. 2.12 c. 54; ANSI
Разд. 3.3 c. 39; CT&P Разд. 3.7 c. 47; PCS Разд. 9.5 c. 120-1.
(Не принимайте во внимание H&S Разд. 7.12 c. 190-1, это устарело)
4.3: Я экспериментировал с кодом
int i = 2;
i = i++;
Некоторые компиляторы выдавали i=2, некоторые 3, но один выдал 4.
Я знаю, что поведение неопределено, но как можно получить 4?
О: Неопределенное (undefined) поведение означает, что может случиться
_все_ что угодно. См. вопрос 5.23.
4.4 Люди твердят, что поведение неопределено, а я попробовал ANSI -
компилятор и получил то, что ожидал.
О: Компилятор делает все, что ему заблагорассудится, когда встречается
с неопределенным поведением (до некоторой степени это относится и к
случаю зависимого от реализации и неописанного поведения). В
частности, он может делать то, что Вы ожидаете. Неблагоразумно,
однако, полагаться на это. См. также вопрос 5.18.
4.5: Могу я использовать круглые скобки, чтобы обеспечить нужный мне
порядок вычислений? Если нет, то разве приоритет операторов не
обеспечивает этого?
О: Круглые скобки, как и приоритет операторов обеспечивают лишь
частичный порядок при вычислении выражений. Рассмотрим выражение
f() + g() * h() .
Хотя известно, что умножение будет выполнено раньше сложения, нельзя
ничего сказать о том, какая из трех функций будет вызвана первой.
4.6 Тогда как насчет операторов &&, ||, и запятой ? Я имею в виду код
типа
if((c = getchar()) == EOF || c == '\n')" ...
О: Для этих операторов, как и для оператора ?: существует специальное
исключение; каждый из них подразумевает определенный порядок
вычислений, т.е. гарантируется вычисление слева-направо. В любой
книге по С эти вопросы должны быть ясно изложены.
См.: K&R I Разд. 2.6 c. 38, Разд. A7.11-12 c. 190-1;
K&R II Разд. 2.6 c. 41, Разд. A7.14-15 c. 207-8; ANSI
Разд. 3.3.13 c. 52, 3.3.14 c. 52, 3.3.15 c. 53, 3.3.17 c. 55,
CT&P Разд. 3.7 c. 46-7.
4.7 Если я не использую значение выражения, то как я должен увеличивать
переменную i: так: ++i или так: i++ ?
О: Применение той или иной формы сказывается только на значении
выражения, обе формы полностью эквивалентны, когда требуются только
их побочные эффекты.
4.8 Почему неправильно работает код
int a = 1000, b = 1000;
long int c = a * b; ?
О: Согласно общим правилам преобразования типов языка С, умножение
выполняется с использованием целочисленной арифметики, и результат
может привести к переполнению и/или усечен до того как будет присвоен
стоящей слева переменной типа long int. Используйте явное приведение
типов, чтобы включить арифметику длинных целых
long int c = (long int)a * b;
Заметьте, что код (long int)(a * b) _не_ приведет к желаемому
результату.
5. ANSI C
5.1 Что такое стандарт ANSI C ?
О: В 1983 году Американский институт национальных стандартов (ANSI)
учредил комитет X3J11, чтобы разработать стандарт языка С. После
длительной и трудной работы, включающей выпуск нескольких публичных
отчетов, работа комитета завершилась 14 декабря 1989 г.созданием
стандарта ANS X3.159-1989. Стандарт был опубликован весной 1990 г.
В большинстве случаев ANSI C узаконил уже существующую практику и
сделал несколько заимствований из С++ (наиболее важное - введение
прототипов функций). Была также добавлена поддержка национальных
наборов символов (включая подвергшиеся наибольшим нападкам
трехзнаковые последовательности). Стандарт ANSI C формализовал также
стандартную библиотеку.
Опубликованный стандарт включает "Комментарии" ("Rationale"), в
которых объясняются многие решения и обсуждаются многие тонкие
вопросы, включая несколько затронутых здесь. ("Комментарии"
не входят в стандарт ANSI X3.159-1989, они приводятся в качестве
дополнительной информации.)
Стандарт ANSI был принят в качестве международного стандарта ISO/IEC
9899:1990, хотя нумерация разделов иная (разделы 2 - 4 стандарта
ANSI соответствуют разделам 5 - 7 стандарта ISO), раздел
"Комментарии" не был включен.
5.2 Как получить копию Стандарта?
ANSI X3.159 был официально заменен стандартом ISO 9899. Копию
стандарта можно получить по адресу
American National Standards Institute
11 W. 42nd St., 13th floor
New York, NY 10036 USA
(+1) 212 642 4900
или
Global Engineering Documents
2805 McGaw Avenue
Irvine, CA 92714 USA
(+1) 714 261 1455
(800) 854 7179 (U.S. & Canada)
В других странах свяжитесь с местным комитетом по стандартам или
обратитесь в Национальный Комитет по Стандартам в Женеве
ISO Sales
Case Postale 56
CH-1211 Geneve 20
Switzerland
Цена составляет в ANSI $130, в Global Engineering Documents - $160.
Копии оригинального стандарта Х3.159, включающие "Комментарии",
попрежнему доступны за $205.00 (ANSI) или за $200.50 (Global
Engineering Documents). Отметим, что комитет ANSI для поддержки своей
деятельности получает доход от продажи отпечатанных копий стандарта,
так что электронные копии _недоступны_.
Книга Герберта Шилдта с обманчивым названием "Комментарии к стандарту
С" содержит лишь несколько страниц стандарта ISO 9899; опубликована
издательством Osborne/McGraw-Hill, ISBN 0-07-881952-0 и продается
примерно за $40. (Есть мнение, что различие в цене между официальным
стандартом и комментарием Герберта Шилдта соответствует ценности
комментария).
Текст "Комментариев" (не всего стандарта) теперь доступен через
ftp ftp.uu.net (см. вопрос 17.12) в директории doc/standards/ansi/
X3.159-1989. "Комментарии" были также изданы издательством Silicon
Press, ISBN 0-929306-07-4.
5.3 Есть ли у кого-нибудь утилиты для перевода С-программ, написанных в
в старом стиле, в ANSI C и наоборот? Существуют ли программы для
автоматического создания прототипов?
О: Две программы, protoize и unprotoize осуществляют преобразование в
обе стороны между функциями, записанными в новом стиле с прототипами,
и функциями, записанными в старом стиле. (Эти программы не
поддерживают полный перевод между "классическим" и ANSI C).
Упомянутые программы были сначала вставками в FSF GNU компилятор С,
gcc, но теперь они - часть дистрибутива gcc; смотри директорий
pub/gnu на prep.ai.mit.edu (18.71.0.38), или в других архивах FSF.
Программа unproto ((/pub/unix/unproto5.shar.Z на ftp.win.tue.nl -
это фильтр, располагающийся между препроцессором и следующим
проходом компилятора - на лету переводит большинство особенностей
ANSI C в традиционный С.
GNU пакет GhostScript содержит маленькую программу ansi2knr.
Есть несколько генераторов прототипов, многие из них - модификации
программы lint. Версия 3 программы CPROTO была помещена в конференцию
comp.sources.misc в марте 1992 г. Есть другая программа, которая
называется "ctxtract". См. вопрос 17.12.
В заключение хочется спросить: так ли уж нужно преобразовывать
огромное количество старых программ в ANSI C? Старый стиль написания
функций все еще допустим.
5.4 Я пытаюсь использовать ANSI- строкообразующий оператор #, чтобы
вставить в сообщение значение символической константы, но вставляется
формальный параметр макроса, а не его значение.
О: Необходимо использовать двухшаговую процедуру для того чтобы макрос
раскрывался как при строкообразовании
#define str(x) #x
#define xstr(x) str(x)
#define OP plus
char *opname = xstr(OP);
Такая процедура устанавливает opname равным "plus", а не "OP".
Такие же обходные маневры необходимы при использовании оператора
склеивания лексем ##, когда нужно соединить значения (а не имена
формальных параметров) двух макросов.
Смотри: ANSI Разд. 3.8.3.2, Разд. 3.8.3.5 пример c. 93.
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 .
9.3 Каков механизм передачи и возврата структур?
О: Структура, передаваемая функции как параметр, обычно целиком
размещается на стеке, используя необходимое количество машинных слов.
(Часто для снижения ненужных затрат программисты предпочитают
передавать функции указатель на структуру вместо самой структуры).
Структуры часто возвращаются функциями в ту область памяти, на
которую указывает дополнительный поддерживаемый компилятором
"скрытый" аргумент. Некоторые старые компиляторы используют для
возврата структур фиксированную область памяти, хотя это делает
невозможным рекурсивный вызов такой функции, что противоречит
стандарту ANSI.
Смотри: ANSI Разд.2.2.3 c. 13.
9.4 Эта программа работает правильно, но после завершения выдает дамп
оперативной памяти. Почему?
struct list
{
char *item;
struct list *next;
}
/* Здесь функция main */
main(argc, argv)
...
О: Из-за пропущенной точки с запятой компилятор считает, что main
возвращает структуру. (Связь структуры с функцией main трудно
определить, мешает комментарий). Так как для возврата структур
компилятор обычно использует в качестве скрытого параметра
указатель, код, сгенерированный для main() пытается принять три
аргумента, хотя передаются (в данном случае стартовым кодом С)
только два. См. также вопрос 17.21.
9.5 Почему нельзя сравнивать структуры?
О: Не существует разумного способа сделать сравнение структур
совместимым с низкоуровневой природой языка С. Побайтовое сравнение
может быть неверным из-за случайных бит в неиспользуемых "дырках"
(такое заполнение необходимо, чтобы сохранить выравнивание для
последующих полей; см. вопросы 9.10 и 9.11). Почленное сравнение
потребовало бы неприемлевого количества повторяющихся машинных
инструкций в случае больших структур.
Если необходимо сравнить две структуры, напишите для этого свою
собственную функцию. C++ позволит создать оператор ==, чтобы
связать его с Вашей функцией.
Смотри: K&R II Разд.6.2 c. 129; H&S Разд. 5.6.2 c. 103; ANSI
Rationale разд. 3.3.9 c. 47.
9.7 Как читать/писать структуры из файла/в файл ?
О: Писать структуры в файл можно непосредственно с помощью fwrite:
fwrite((char *)&somestruct, sizeof(somestruct), 1, fp);
a cоответствующий вызов fread прочитает структуру из файла.
Однако файлы, записанные таким образом будут _не_ особенно переносимы
(см. вопросы 9.11 и 17.3). Заметьте также, что на многих системах
нужно использовать в функции fopen флаг "b" .
9.7 Мне попалась программа, в которой структура определяется так:
struct name
{
int namelen;
char name[1];
};
затем идут хитрые манипуляции с памятью, чтобы массив name вел себя
будто в нем несколько элементов. Такие манипуляции законны/мобильны?
О: Такой прием популярен, хотя Деннис Ритчи назвал это "слишком
фамильярным обращением с реализацией С". ANSI полагает, что выход за
пределы объявленного размера члена структуры не полностью соответствует
стандарту, хотя детальное обсуждение всех связанных с этим проблем
не входит в задачу данных вопросов и ответов. Похоже, однако, что
описанный прием будет одинаково хорошо принят всеми известными
реализациями С. (Компиляторы, тщательно проверяющие границы массивов,
могут выдать предупреждения). Для страховки будет лучше объявить
переменную очень большого размера чем очень малого. В нашем случае
...
char name[MAXSIZE];
...
где MAXSIZE больше, чем длина любого имени, которое будет сохранено
в массиве name[]. (Есть мнение, что такая модификация будет
соответствовать Стандарту).
Смотри: ANSI Rationale Разд. 3.5.4.2 c. 54-5.
9.8 Как определить смещение члена структуры в байтах?
О: Если это возможно, необходимо использовать макрос offsetof, который
определен стандартом ANSI; см. <stddef.h>. Если макрос отсутствует,
предлагается такая (не на все 100% мобильная) его реализация
#define offsetof(type, mem) ((size_t) \
((char *)&((type *) 0)->mem - (char *)((type *) 0)))
Для некоторых компиляторов использование этого макроса может оказаться
незаконным.
О том, как использовать offsetof(), смотри следующий вопрос.
Смотри: ANSI Разд. 4.1.5, Rationale Разд. 3.5.4.2 c. 55.
9.9 Как осуществить доступ к членам структур по их именам во время
выполнения программы?
О: Создайте таблицу имен и смещений, используя макрос offsetof().
Смещение члена структуры b в структуре типа a равно
offsetb = offsetof(struct a, b)
Если structp указывает на начало структуры, а b - член структуры типа
int, смещение которого получено выше, b может быть установлен
косвенно с помощью
*(int *)((char *)structp + offsetb) = value;
9.10 Почему sizeof выдает больший размер структурного типа, чем я ожидал,
как будто в конце структры лишние символы?
О: Это происходит (возможны также внутренние "дыры" ; см.
также вопрос 9.5), когда необходимо выравнивание при задании массива
непрерывных структур.
9.11 Мой компилятор оставляет дыры в структурах, что приводит к потере
памяти и препятствует "двоичному" вводу/выводу при работе с
внешними файлами. Могу я отключить "дырообразование" или как-то
контролировать выравнивание?
О: В Вашем компиляторе, возможно, есть расширение, (например,
#pragma), которое позволит это сделать, но стандартного способа не
существует. См. также вопрос 17.3.
9.12 Можно ли задавать начальные значения объединений?
О: Стандарт ANSI допускает инициализацию первого члена объединения.
Не существует стандартного способа инициализации других членов.
(и тем более нет такого способа для старых компиляторов, которые
вообще не поддерживают какой-либо инициализации).
9.13 Как передать функциии структуру, у которой все члены - константы?
О: Поскольку в языке С нет возможности создавать безымянные значения
структурного типа, необходимо создать временную структуру.
10. Декларации
10.1 Какой тип целочисленной переменной использовать?
О: Если могут потребоваться большие числа, (больше 32767 или меньше
-32767), используйте тип long. Если нет, и важна экономия памяти
(большие массивы или много структур), используйте short. Во всех
остальных случаях используйте int. Если важно точно определить
момент переполнения и/или знак числа не имеет значения, используйте
соответствующий тип unsigned. (Но будьте внимательны при совместном
использовании типов signed и unsigned в выражениях). Похожие
соображения применимы при выборе между float и double.
Хотя тип char или unsigned char может использоваться как
целочисленный тип наименьшего размера, от этого больше вреда,
чем пользы из-за непредсказуемых перемен знака и возрастающего
размера программы.
Эти правила, очевидно, не применимы к адресам переменных, поскольку
адрес должен иметь совершенно определенный тип.
Если необходимо объявить переменную _определенного_ размера,
(единственной причиной тут может быть попытка удовлетворить внешним
требованиям к организации памяти; см.,кроме того, вопрос 17.3),
непременно изолируйте объявление соответствующим typedef.
10.2 Каким должен быть новый 64-битный тип на новых 64-битных машинах?
О: Некоторые поставщики С компиляторов для 64-битных машин поддерживают
тип long int длиной 64 бита. Другие же, опасаясь, что слишком многие
уже написанные программы зависят от sizeof(int) == sizeof(long) == 32
бита, вводят новый 64-битный тип long long (или __longlong).
Программисты, желающие писать мобильные программы, должны,
следовательно, изолировать 64-битные типы с помощью средства typedef.
Разработчики компиляторов, чувствующие необходимость ввести новый
целочисленный тип большего размера, должны объявить его как "имеющий
по крайней мере 64 бит" (это действительно новый тип, которого нет
в традиционном С), а не как "имеющий точно 64 бит".
10.3 У меня совсем не получается определение связанного списка. Я пишу
typedef struct
{
char *item;
NODEPTR next;
} *NODEPTR;
но компилятор выдает сообщение об ошибке. Может структура в С
содержать ссылку на себя?
О: Структуры в С, конечно же, могут содержать указатели на себя;
обсуждение этого вопроса и пример в параграфе 6.5 K&R вполне
проясняют этот вопрос. В приведенном тексте проблема состоит в том,
что определение NODEPTR не закончено в том месте, где объявлется
член структуры "next". Для исправления, снабдите сначала структуру
тегом ("struct node"). Далее объявите "next" как "struct node
*next;", и/или поместите декларацию typedef целиком до или целиком
после объявления структуры. Одно из возможных решений будет таким:
struct node
{
char *item;
struct node *next;
};
typedef struct node *NODEPTR;
Есть по крайней мере три других одинаково правильных способа
сделать то же самое.
Сходная проблема, которая решается примерно так же, может возникнуть
при попытке определить с помощью средства typedef пару cсылающихся
друг на друга структур.
Смотри: K&R I Разд. 6.5 c. 101; K&R II Разд. 6.5 c. 139; H&S
Разд. 5.6.1 c. 102; ANSI Разд. 3.5.2.3 .
10.4 Как объявить массив из N указателей на функции, возвращающие
указатели на функции возвращающие указатели на char?
О: Есть по крайней мере три варианта ответа:
1. char *(*(*a[N])())();
2. Писать декларации по шагам, используя typedef:
typedef char *pc; /* указатель на char */
typedef pc fpc(); /* функция,возвращающая указатель на char */
typedef fpc *pfpc; /* указатель на.. см. выше */
typedef pfpc fpfpc(); /* функция, возвращающая... */
typedef fpfpc *pfpfpc; /* указатель на... */
pfpfpc a[N]; /* массив... */
3. Использовать программу cdecl, которая переводит с английского
на С и наоборот.
cdecl> declare a as array of pointer to function returning
pointer to function returning pointer to char
char *(*(*a[])())()
cdecl может также объяснить сложные декларации, помочь при явном
приведении типов, и, для случая сложных деклараций, вроде только что
разобранного, показать набор круглых скобок, в которые заключены
аргументы. Версии cdecl можно найти в comp.sources.unix (см. вопрос
17.12) и в K&R II. Любая хорошая книга по С должна объяснять, как
для понимания сложных деклараций, читать их "изнутри наружу",
("декларация напоминает использование").
Смотри: K&R II Разд. 5.12 c. 122; H&S Разд. 5.10.1 c. 116.
10.5 Я моделирую Марковский процесс с конечным числом состояний, и у меня
есть набор функций для каждого состояния. Я хочу, чтобы смена
состояний происходила путем возврата функцией указателя на функцию,
соответветствующую следующему состоянию. Однако, я обнаружил
ограничение в механизме деклараций языка С: нет возможности объявить
функцию, возвращающую указатель на функцию, возвращающую указатель
на функцию, возвращающую указатель на функцию...
О: Да, непосредственно это сделать нельзя. Пусть функция возвращает
обобщенный указатель на функцию, к которому перед вызовом функции
будет применен оператор приведения типа, или пусть она возвращает
структуру, содержащую только указатель на функцию, возвращающую
эту структуру.
10.6 Мой компилятор выдает сообщение о неверной повторной декларации,
хотя я только раз определил функцию и только раз вызвал.
О: Подразумевается, что функции, вызываемые без декларации в области
видимости (или до такой декларации), возвращают значение типа int.
Это приведет к противоречию, если впоследствии функция декларирована
иначе. Если функция возвращает нецелое значение, она должна быть
объявлена до того как будет вызвана.
Смотри: K&R I Разд. 4.2 c. 70; K&R II Разд. 4.2 c. 72; ANSI
Разд. 3.3.2.2 .
10.7 Как наилучшим образом декларировать и определить глобальные
переменные?
О: Прежде всего заметим, что хотя может быть много _деклараций_ (и во
многих файлах) одной "глобальной" (строго говоря "внешней" )
переменной, (или функции), должно быть всего одно _определение_.
(Определение - это такая декларация, при которой действительно
выделяется память для переменной, и присваивается, если нужно,
начальное значение). Лучше всего поместить определение в какой-то
главный (для программы или ее части) .c файл, с внешней декларацией в
головном файле .h, который при необходимости подключается с помощью
#include. Файл, в котором находится определение переменной, также
должен включать головной файл с внешней декларацией, чтобы компилятор
мог проверить соответствие декларации и определения.
Это правило обеспечивает высокую мобильность программ и находится в
согласии с требованиями стандарта ANSI C. Заметьте, что многие
компиляторы и компоновщики в системе UNIX используют "общую модель",
которая разрешает многократные определения без инициализации.
Некоторые весьма странные компиляторы могут требовать явной
инициализации, чтобы отличить определение от внешней декларации.
С помощью препроцессорного трюка можно устроить так, что декларация
будет сделана лишь однажды, в головном файле, и она c помощью #define
"превратится" в определение точно при одном включении головного
файла.
Смотри: K&R I Разд. 4.5 c. 76-7; K&R II Разд. 4.4 c. 80-1;
ANSI Разд. 3.1.2.2 (особенно Rationale), Разд. 3.7, 3.7.2,
Разд. F.5.11; H&S Разд. 4.8 c. 79-80; CT&P Разд. 4.2 c. 54-56.
10.8 Что означает ключевое слово extern при декларации функции?
О: слово extern при декларации функции может быть использовано из
соображений хорошего стиля для указания на то, что определение
функции, возможно, находится в другом файле. Формально между
extern int f();
и
int f();
нет никакой разницы.
Смотри: ANSI Разд. 3.1.2.2 .
10.9 Я, наконец, понял, как объвлять указатели на функции, но как их
инициализировать?
О: Используйте нечто такое
extern int func();
int (*fp)() = func;
Когда имя функции появляется в выражении, но функция не вызывается
(то есть, за именем функции не следует "(" ), оно "сворачивается",
как и в случае массивов, в указатель (т.е. неявным образом записанный
адрес).
Явное объявление функции обычно необходимо, так как неявного
объявления внешней функции в данном случае не происходит (опять-таки
из-за того, что за именем функции не следует "(" ).
10.10 Я видел, что функции вызываются с помощью указателей и просто как
функции. В чем дело?
О: По первоначальному замыслу создателя С указатель на функцию должен
был "превратиться" в настоящую функцию с помощью оператора *
и дополнительной пары круглых скобок для правильной интерпретации.
int r, func(), (*fp)() = func;
r = (*fp)();
На это можно возразить, что функции всегда вызываются с помощью
указателей, но что "настоящие" функции неявно превращаются в
указатели (в выражениях, как это происходит при инициализациях) и это
не приводит к каким-то проблемам. Этот довод, широко распространенный
компилятором pcc и принятый стандартом ANSI, означает, что выражение
r = fp();
работает одинаково правильно, независимо от того, что такое fp -
функция или указатель на нее. (Имя всегда используется однозначно;
просто невозможно сделать что-то другое с указателем на функцию,
за которым следует список аргументов, кроме как вызвать функцию).
Явное задание * безопасно и все еще разрешено (и рекомендуется,
если важна совместимость со старыми компиляторами).
Смотри: ANSI Разд. 3.3.2.2 c. 41, Rationale c. 41.
10.11 Где может пригодиться ключевое слово auto?
О: Нигде, оно вышло из употребления.
11. Cтандартный ввод/вывод.
11.1 Что плохого в таких строках:
char c;
while((c = getchar()) != EOF)...
О: Во-первых, переменная, которой присваивается возвращенное getchar
значение, должна иметь тип int. getchar может вернуть все возможные
значения для символов, в том числе EOF. Если значение, возвращенное
getchar присваивается переменной типа char, возможно либо обычную
литеру принять за EOF, либо EOF исказится (особенно если использовать
тип unsigned char) так, что распознать его будет невозможно.
Смотри: CT&P Разд.5.1 c. 70.
11.2 Как напечатать символ '%' в строке формата printf? Я попробовал
\%, но из этого ничего не вышло.
О: Просто удвойте знак процента %% .
Смотри: K&R I Разд. 7.3 c. 147; K&R II Разд. 7.2 c. 154; ANSI
Разд. 4.9.6.1 .
11.3 Почему не работает scanf("%d",i)?
О: Для функции scanf необходимы адреса переменных, по которым будут
записаны данные, нужно написать scanf("%d", &i);
11.4 Почему не работает
double d;
scanf("%f", &d);
О: scanf использует спецификацию формата %lf для значений типа double
и %f для значений типа float. (Обратите внимание на несходство с
printf, где в соответствии с правилом расширения типов аргументов
спецификация %f используется как для float, так и для double).
11.5 Почему фрагмент программы
while(!feof(infp)) {
fgets(buf, MAXLINE, infp);
fputs(buf, outfp);
}
дважды копирует последнюю строку?
О: Это Вам не Паскаль. Символ EOF появляется только _после_ попытки
прочесть, когда функция ввода натыкается на конец файла.
Чаще всего необходимо просто проверять значение, возвращаемое
функцией ввода, (в нашем случае fgets); в использовании feof()
обычно вообще нет необходимости.
11.6 Почему все против использования gets()?
О: Потому что нет возможности предотвратить переполнение буфера,
куда читаются данные, ведь функции gets() нельзя сообщить его
размер. Смотрите вопрос 3.1, в котором приведен фрагмент программы,
показывающий замену gets() на fgets().
11.7 Почему переменной errno присваивается значение ENOTTY после вызова
printf()?
О: Многие реализации стандартной библиотеки ввода/вывода несколько
изменяют свое поведение, если стандартное устройство вывода -
терминал. Чтобы определить тип устройства, выполняется операция,
которая оканчивается неудачно (c сообщением ENOTTY), если устройство
вывода - не терминал. Хотя вывод завершается успешно, errno все же
содержит ENOTTY.
Смотри: CT&P Разд. 5.4 c. 73.
11.8 Запросы моей программы, а также промежуточные результаты не всегда
отображаются на экране, особенно когда моя программа передает данные
по каналу (pipe) другой программе.
О: Лучше всего явно использовать fflush(stdout), когда непременно
нужно видеть то, что выдает программа. Несколько механизмов пытаются
"в нужное время" осуществить fflush, но, похоже, все это правильно
работает в том случае, когда stdout - это терминал. (см. вопрос
11.7).
11.9 При чтении с клавиатуры функцией scanf возникает чувство, что
программа зависает, пока я перевожу строку.
О: Функция scanf была задумана для ввода в свободном формате,
необходимость в котором возникает редко при чтении с клавиатуры.
Что же касается ответа на вопрос, то символ "\n" в форматной строке
вовсе не означает, что scanf будет ждать перевода строки. Это
значит, что scanf будет читать и отбрасывать все встретившиеся подряд
пробельные литеры (т.е. символы пробела, табуляции, новой строки,
возврата каретки, вертикальной табуляции и новой страницы).
Похожее затруднение случается, когда scanf "застревает", получив
неожиданно для себя нечисловые данные. Из-за подобных проблем часто
лучше читать всю строку с помощью fgets, а затем использовать sscanf
или другие функции, работающие со строками, чтобы интерпретировать
введенную строку по частям. Если используется sscanf, не забудьте
проверить возвращаемое значение для уверенности в том, что число
прочитанных переменных равно ожидаемому.
11.10 Я пытаюсь обновить содержимое файла, для чего использую fopen в
режиме "r+", далее читаю строку, затем пишу модифицированную строку
в файл, но у меня ничего не получается.
О: Непременно вызовите fseek перед записью в файл. Это делается
для возврата к началу строки, которую Вы хотите переписать; кроме
того, всегда необходимо вызвать fseek или fflush между чтением и
записью при чтении/записи в режимах "+". Помните также, что литеры
можно заменить лишь точно таким же числом литер. См. также
вопрос 17.4.
Смотри: ANSI Разд. 4.9.5.3 c. 131.
11.11 Как мне прочитать одну литеру, не дожидаясь нажатия RETURN?
О: Смотри вопрос 16.1
11.12 Как мне отменить ожидаемый ввод, так, чтобы данные, введенные
пользователем, не читались при следующем запросе? Поможет ли здесь
fflush(stdin)?
О: fflush определена только для вывода. Поскольку определение "flush"
("смывать") означает завершение записи символов из буфера (а не
отбрасывание их), непрочитанные при вводе символы не будут уничтожены
с помощью fflush. Не существует стандартного способа игнорировать
символы, еще не прочитанные из входного буфера stdio. Не видно также,
как это вообще можно сделать, поскольку непрочитанные символы могут
накапливаться в других, зависящих от операциооной системы, буферах.
11.13 Как перенаправить stdin или stdout в файл?
О: Используйте freopen.
11.14 Если я использовал freopen, то как вернуться назад к stdout (stdin)?
О: Если необходимо переключаться между stdin (stdout) и файлом,
наилучшее универсальное решение - не спешить использовать freopen.
Попробуйте использовать указатель на файл, которому можно по желанию
присвоить то или иное значение, оставляя значение stdout (stdin)
нетронутым.
11.15 Как восстановить имя файла по указателю на открытый файл?
О: Это проблема, вообще говоря, неразрешима. В случае операционной
системы UNIX, например, потребуется поиск по всему диску (который,
возможно, потребует специального разрешения), и этот поиск окончится
неудачно, если указатель на файл был каналом (pipe) или был связан
с удаленным файлом. Кроме того, обманчивый ответ будет получен для
файла со множественными связями. Лучше всего самому запоминать имена
при открытии файлов (возможно, используя специальные функции,
вызываемые до и после fopen);
12. Библиотечные функции.
12.1 Почему strncpy не всегда завершает строку-результат символом '\0'?
О: strncpy была задумана для обработки теперь уже устаревших структур
данных - "строк" фиксированной длины, не обязательно завершающихся
символом '\0'. И, надо сказать, strncpy не совсем удобно
использовать в других случаях, поскольку часто придется добавлять
символ '\0' вручную.
12.2 Я пытаюсь сортировать массив строк с помощью qsort, используя
для сравнения strcmp, но у меня ничего не получается.
О: Когда Вы говорите о "массиве строк", то, видимо, имеете в виду
"массив указателей на char". Аргументы функции сравнения, работающей
в паре с qsort - это указатели на сравниваемые объекты, в данном
случае - указатели на указатели на char. (Конечно, strcmp работает
просто с указателями на char).
Аргументы процедуры сравнения описаны как "обобщенные указатели"
const void * или char *. Они должны быть превращены в то, что они
представляют на самом деле, т.е. (char **) и дальше нужно раскрыть
ссылку с помощью * ; тогда strcmp получит именно то, что нужно для
сравнения. Напишите функцию сравнения примерно так:
int pstrcmp(p1, p2) /* сравнить строки, используя указатели */
char *p1, *p2; /* const void * для ANSI C */
{
return strcmp(*(char **)p1, *(char **)p2);
}
Имейте в виду, что в K&R II Разд. 5.11 обсуждается функция qsort,
которая отличается от стандартной.
12.3 Сейчас я пытаюсь сортировать массив структур с помощью qsort.
Процедура сравнения, которую я использую, принимает в качестве
аргументов указатели на структуры, но компилятор выдает сообщение
о неверном типе функции сравнения. Как мне преобразовать аргументы
функции, чтобы подавить сообщения об ошибке?
О: Преобразования должны быть сделаны внутри функци сравнения, которая
должна быть объявлена как принимающая аргументы типа "обобщенных
указателей (const void * или char *) как это описано в вопросе 12.2.
Функция сравнения может выглядеть так:
int mystructcmp(p1, p2)
char *p1, *p2; /* const void * для ANSI C */
{
struct mystruct *sp1 = (struct mystruct *)p1;
struct mystruct *sp2 = (struct mystruct *)p2;
/* теперь сравнивайте sp1->что-угодно и sp2-> ... */
}
(С другой стороны, если сортируются указатели на структуры,
необходима косвенная адресация, как в вопросе 12.2:
sp1 = *(struct mystruct **)p1 .)
12.4 Как преобразовать числа в строки (операция, противоположная atoi)?
Есть ли функция itoa?
О: Просто используйте sprintf. (Необходимо будет выделить память для
результата, см. вопросы 3.1 и 3.2. Беспокоиться, что sprintf -
слишком сильное средство, которое может привести к перерасходу
памяти и увеличению времени выполнения, нет оснований. На практике
sprintf работает хорошо).
Смотри: K&R I Разд.3.6 c. 60; K&R II Разд.3.6 c. 64.
12.5 Как получить дату или время в С программе?
О: Просто используйте функции time, ctime, и/или localtime. (Эти
функции существуют многие годы, они включены в стандарт ANSI).
Вот простой пример:
#include <stdio.h>
#include <time.h>
main()
{
time_t now = time((time_t *)NULL);
printf("It's %.24s.\n", ctime(&now));
return 0;
}
Смотри: ANSI Разд. 4.12 .
12.6 Я знаю, что библиотечная функция localtime разбивает значение time_t
по отдельным членам структуры tm, а функция ctime превращает time_t в
строку символов. А как проделать обратную операцию перевода
структуры tm или строки символов в значение time_t?
О: Стандарт ANSI определяет библиотеную функцию mktime, которая
преобразует структуру tm в time_t. Если Ваш компилятор не
поддерживает mktime, воспользуйтесь одной из общедоступных версий
этой функции.
Перевод строки в значение time_t выполнить сложнее из-за большого
количества форматов дат и времени, которые должны быть распознаны.
Некоторые компиляторы поддерживают функцию strptime; другая
популярная функция - partime широко распространяется с пакетом RCS,
но нет уверенности, что эти функции войдут в Стандарт.
Смотри: K&R II Разд. B10 c. 256; H&S Разд. 20.4 c. 361; ANSI
Разд. 4.12.2.3 .
12.7 Как прибавить n дней к дате? Как вычислить разность двух дат?
О: Вошедшие в стандарт ANSI/ISO функции mktime и difftime могут помочь
при решении обеих проблем. mktime() поддерживает ненормализованные
даты, т.е. можно прямо взять заполненную структуру tm, увеличить или
уменьшить член tm_mday, затем вызвать mktime(), чтобы нормализовать
члены year, month, и day (и преобразовать в значение time_t).
difftime() вычисляет разность в секундах между двумя величинами
типа time_t. mktime() можно использовать для вычисления значения
time_t разности двух дат. (Заметьте, однако, что все эти приемы
возможны лишь для дат, которые могут быть представлены значением
типа time_t; кроме того, из-за переходов на летнее и зимнее время
продолжительность дня не точно равна 86400 сек.).
Cм. также вопросы 12.6 и 17.28.
Смотри: K&R II Разд. B10 c. 256; H&S Разд. 20.4, 20.5
c. 361-362; ANSI Разд. 4.12.2.2, 4.12.2.3 .
12.8 Мне нужен генератор случайных чисел.
О: В стандартной библиотеке С есть функция rand(). Реализация этой
функции в Вашем компиляторе может не быть идеальной, но и создание
лучшей функции может оказаться очень непростым.
Смотри: ANSI Разд. 4.10.2.1 c. 154; Knuth Vol. 2 Chap. 3
c. 1-177.
12.9 Как получить случайные целые числа в определенном диапазоне?
О: Очевидный способ
rand() % N
где N, конечно, интервал, довольно плох, ведь поведение младших
бит во многих генераторах случайных чисел огорчает своей
неслучайностью. (См. вопрос 12.11). Лучше попробуйте нечто вроде
(int)((double)rand() / ((double)RAND_MAX + 1) * N)
Если Вам не нравится употребление чисел с плавающей точкой,
попробуйте
rand() / (RAND_MAX / N + 1)
Оба метода требуют знания RAND_MAX (согласно ANSI, RAND_MAX определен
в <stdlib.h>. Предполагается, что N много меньше RAND_MAX.
12.10 Каждый раз при запуске программы функция rand() выдает одну и ту же
последовательность чисел.
О: Можно вызвать srand() для случайной инициализации генератора
случайных чисел. В качестве аргумента для srand() часто используется
текущее время, или время, прошедшее до нажатия на клавишу (хотя едва
ли существует мобильная процедура определения времен нажатия на
клавиши; см. вопрос 16.10).
Смотри: ANSI Разд. 4.10.2.2 c. 154.
12.11 Мне необходима случайная величина, имеющая два значения true/false.
Я использую rand() % 2, но получается неслучайная последовательность
0,1,0,1,0....
О: Некачественные генераторы случайных чисел (попавшие, к несчастью, в
состав некоторых компиляторов) не очень то случайны, когда речь
идет о младших битах. Попробуйте использовать старшие биты.
См. вопрос 12.9.
12.12 Я пытаюсь перенести на О: Эти подпрограммы в разной
другую систему старую степени устарели. Необходимо
программу. Почему я использовать
получаю сообщения
"undefined external"
для
index? используйте strchr.
rindex? используйте strrchr.
bcopy? используйте memmove,
поменяв местами первый
и второй аргументы (см. также
вопрос 5.15).
bcmp? используйте memcmp.
bzero? используйте memset, со вторым
аргументом, равным 0.
12.13 Я все время получаю сообщения об ошибках - не определены библиотечные
функции, но я включаю все необходимые головные файлы.
О: Иногда (особенно для нестандартных функций) следует явно указывать,
какие библиотеки нужны при компоновке программы.
См. также вопрос 15.2.
12.14 Я по-прежнему получаю сообщения, что библиотечные функции не
определены, хотя и использую ключ -l, чтобы явно указать библиотеки
во время компоновки.
О: Многие компоновщики делают один проход по списку объектных файлов
и библиотек, которые Вы указали, извлекая из библиотек только те
функции, удовлетворяющие ссылки, которые _к этому моменту_ оказались
неопределенными. Следовательно, порядок относительно объектных файлов,
в котором перечислены библиотеки, важен; обычно просмотр библиотек
нужно делать в самом конце. (Например, в операционной системе UNIX
помещайте ключи -l в самом конце командной строки).
12.15 Мне необходим исходный текст программы, которая осуществляет поиск
заданной строки.
О: Ищите библиотеку regesp (поставляется со многими UNIX - системами)
или достаньте пакет regexp Генри Спенсера (Henry Spencer)
(cs.toronto.edu директорий pub/regexp.shar.Z).
См. также вопрос 17.12.
12.16 Как разбить командную строку на разделенные пробельными литерами
аргументы (что-то вроде argc и argv в main)?
О: В большинстве компиляторов имеется функция strtok, хотя она
требует хитроумного обращения, а ее возможности могут Вас не
удовлетворить (например, работа в случае кавычек).
Смотри: ANSI Разд. 4.11.5.8; K&R II Разд.B3 c. 250; H&S
Разд. 15.7; PCS c. 178.
13. Lint.
13.1 Вот я написал программу, а она ведет себя странно. Что в ней не так?
О: Попробуйте сначала запустить lint (возможно, с ключами -a, -c, -h,
-p). Многие компиляторы С выполняют на самом деле только половину
задачи, не сообщая о тех подозрительных местах в тексте программы,
которые не препятствуют генерации кода.
13.2 Как мне подавить сообщение "warning: possible pointer alignment
problem" ("предупреждение: возможна проблема с выравниванием
указателя"), которое выдает lint после каждого вызова malloc?
О: Проблема состоит в том, что lint обычно не знает, и нет возможности
ему об этом сообщить, что malloc "возвращает указатель на область
памяти, которая должным образом выравнена для хранения объекта любого
типа". Возможна псевдореализация malloc с помощью #define внутри
#ifdef lint, которая удалит это сообщение, но слишком прямолинейное
применение #define может подавить и другие осмысленные сообщения о
действительно некорректных вызовах. Возможно, будет проще игнорировать
эти сообщения, может быть, делать это автоматически с помощью grep -v.
13.3 Где найти ANSI-совместимый lint?
О: Программа, которая называется FlexeLint (в виде исходного текста с
удаленными комментариями и переименованными переменными, пригодная
для компиляции на "почти любой" системе) может быть заказана по
адресу
Gimpel Software
3207 Hogarth Lane
Collegeville, PA 19426 USA
(+1) 610 584 4261
gimpel@netaxs.com
Lint для System V release 4 ANSI-совместим и может быть получен
(вместе с другими C утилитами) от UNIX Support Labs или от
дилеров System V.
Другой ANSI-совместимый LINT (способный также выполнять формальную
верификацию высокого уровня) называется LCLint и доступен через
ftp: larch.lcs.mit.edu://pub/Larch/lclint/ .
Ничего страшного, если программы lint нет. Многие современные
компиляторы почти столь же эфффективны в выявлении ошибок и
подозрительных мест, как и lint.
14. Стиль.
14.1 Может ли простой и приятный трюк
if(!strcmp(s1, s2))
служить образцом хорошего стиля?
О: Стиль не особенно хороший, хотя такая конструкция весьма популярна.
Тест удачен в случае равенства строк, хотя по виду условия можно
подумать, что это тест на неравенство.
Есть альтернативный прием, связанный с использованием макроса
#define Streq(s1, s2) (strcmp((s1), (s2)) == 0)
Вопросы стиля программирования, как и проблемы веры, могут
обсуждаться бесконечно. К хорошему стилю стоит стремиться, он легко
узнаваем, но не определим.
14.2 Каков наилучший стиль внешнего оформления программы?
О: K&R, приводя пример, которому чаще всего следуют, снабдили его
примечанием, предоставляющим Вам окончательный выбор:
Положение скобок менее важно, хотя люди склонны проявлять
фанатизм в таких вопросах. Мы выбрали один из нескольких
популярных стилей. Выберите тот стиль, который Вам больше
подходит, и точно ему следуйте.
Не так важно, чтобы стиль был "идеален". Важнее, чтобы он применялся
последовательно и был совместим (со стилем коллег или общедоступных
программ). Если требования к программированию (местные правила или
требования фирмы) не касаются стиля, и Вы не испытываете желания
изобрести свой собственный стиль, то просто следуйте K&R.
(Давайте не будем повторять бесконечные споры о преимуществах и
недостатках того или иного расположения отступов и скобок. См. также
Indian Hill Style Guide).
Так трудно определимое понятие "хороший стиль" включает в себя
гораздо больше, чем просто внешнее оформление программы; не тратьте
слишком много времени на отступы и скобки в ущерб более существенным
слагаемым качества.
Смотри: K&R Разд. 1.2 c. 10.
14.3 Где достать "Indian Hill Style Guide" и другие рекомендации по стилю
программирования?
О: Различные документы доступны через ftp:
Сервер: Файл или директорий:
cs.washington.edu ~ftp/pub/cstyle.tar.Z
(128.95.1.4) (переработанный Indian Hill guide)
cs.toronto.edu doc/programming
ftp.cs.umd.edu pub/style-guide
15. Операции с плавающей точкой.
15.1 У меня операции с плавающей точкой выполняются странно, и на разных
машинах получаются различные результаты.
О: Сначала убедитесь, что подключен головной файл <math.h> и правильно
объявлены другие функции, возвращающие тип double.
Если дело не в этом, вспомните, что большинство компьютеров используют
форматы с плавающей точкой, которые хотя и похоже, но вовсе не
идеально имитируют операции с действительными числами. Потеря
значимости, накопление ошибок и другие свойственные ЭВМ особенности
вычислений могут быть весьма болезненными.
Не нужно предполагать, что результаты операций с плавающей точкой
будут точными, в особенности не стоит проверять на равенство два числа
с плавающей точкой. (Следует избегать любых ненужных случайных
факторов.)
Все эти проблемы одинаково свойственны как С, так и другим языкам
программирования. Семантика операций с плавающей точкой определяется
обычно так, "как это выполняет процессор"; иначе компилятор вынужден
бы был заниматься непомерно дорогостоящей эмуляцией "правильной"
модели вычислений.
В этих вопросах и ответах нет возможности даже бегло перечислить все
затруднения при вычислениях с плавающей точкой и способы их
преодоления. Хорошая книга по программированию должна содержать
введение в эту область.
Смотри: EoPS Разд. 6 c. 115-8.
15.2 Я пытаюсь проделать кое-какие вычисления, связанные с тригонометрией,
включаю <math.h>, но все равно получаю сообщение: "undefined: _sin"
во время компиляции.
О: Убедитесь в том, что компоновщику известна библиотека, в которой
собраны математические функции. Например, в операционной системе
UNIX часто необходим ключ -lm в самом конце командной строки.
См. также вопрос 12.14.
15.3 Почему в языке С нет оператора возведения в степень?
О: Потому что немногие процессоры имеют такую инструкцию. Вместо этого
можно, включив головной файл <math.h>, использовать функцию pow(),
хотя часто при небольших целых порядках явное умножение
предпочтительней.
Смотри: ANSI Разд. 4.5.5.1 .
15.4 Как округлять числа?
А: Вот самый простой и честный способ:
(int)(x + 0.5)
Хотя для отрицательных чисел это не годится.
15.5 Как выявить специальное значение IEEE NaN и другие специальные
значения?
О: Многие компиляторы с высококачественной реализацией стандарта IEEE
операций с плавающей точкой обеспечивают возможность (например, макрос
isnan()) явной работы с такими значениями, а Numerical C Extensions
Group (NCEG) занимается стандартизацией таких средств. Примером
грубого, но обычно эффектиного способа проверки на NaN служит макрос
#define isnan(x) ((x) != (x))
хотя не знающие об IEEE компиляторы могут выбросить проверку в
процессе оптимизации.
15.6 У меня проблемы с компилятором Turbo C. Программа аварийно
завершается, выдавая нечто вроде "floating point formats not linked."
О: Некоторые компиляторы для мини-эвм, включая Turbo C (а также
компилятор Денниса Ритчи для PDP-11), не включают поддержку операций
с плавающей точкой, когда им кажется, что это не понадобится.
В особенности это касается версий printf и scanf, когда для экономии
места не включается поддержка %e, %f, и %g. Бывает так, что
эвристической процедуры Turbo C, которая определяет - использует
программа операции с плавающей точкой или нет, оказывается
недостаточно, и программист должен лишний раз вызвать функцию,
использующую операции с плавающей точкой, чтобы заставить компилятор
включить поддержку таких операций.
16. Интерфейс с операционной системой.
16.1 Как прочитать с клавиатуры один символ, не дожидаясь новой строки?
О: Вопреки популярному убеждению и желанию многих, этот вопрос (как и
родственные вопросы, связанные с дублированием символов) не относится
к языку С. Передача символов с "клавиатуры" программе, написанной на
С, осуществляется операционной системой, эта операция не
стандартизирована языком С. Некоторые версии библиотеки curses
содержат функцию cbreak(), которая делает как раз то, что нужно.
Если Вы пытаетесь прочитать пароль с клавиатуры без вывода его на
экран, попробуйте getpass(). В операционной системе UNIX используйте
ioctl для смены режима работы драйвера терминала(CBREAK или RAW для
"классических" версий; ICANON, c_cc[VMIN] и с_сс[VTIME] для System V
или Posix). В системе MS-DOS используйте getch(). В системе VMS
попробуйте функции управления экраном (SMG$) или curses, или
используйте низкоуровневые команды $QIO с кодами IO$_READVBLK (и,
может быть, IO$M_NOECHO) для приема одного символа за раз. В других
операционных системах выкручивайтесь сами. Помните, что в некоторых
операционных системах сделать нечто подобное невозможно, так как
работа с символами осуществляется вспомогательными процессорами и
не находится под контролем центрального процессора.
Вопросы, ответы на которые зависят от операционной системы,
неуместны в comp.lang.c. Ответы на многие вопросы можно найти в FAQ
таких групп как comp.unix.questions и comp.os.msdos.programmer.
Имейте в виду, что ответы могут отличаться даже в случае разных
вариантов одной и той же операционной системы. Если вопрос касается
специфики операционной системы, помните, что ответ, пригодный в Вашей
системе, может быть бесполезен всем остальным.
Смотри: PCS Разд. 10 c. 128-9, Разд. 10.1 c. 130-1.
16.2 Как определить - есть ли символы для чтения (и если есть, то сколько?)
И наоборот, как сделать, чтобы выполнение программы не блокировалось,
когда нет символов для чтения?
О: Ответ на эти вопросы также целиком зависит от операционной системы.
В некоторых версиях curses есть функция nodelay(). В зависимости
от операционной системы Вы сможете использовать "неблокирующий ввод/
вывод" или системный вызов "select" или ioctl FIONREAD, или kbhit(),
или rdchk(), или опцию O_NDELAY функций open() или fcntl().
16.3 Как очистить экран? Как выводить на экран негативное изображение?
О: Это зависит от типа терминала (или дисплея). Можете использовать
такую библиотеку как termcap или curses, или какие-то другие
функциии, пригодные для данной операционной системы.
16.4 Как узнать состояние мыши?
О: Посмотрите в системной документации, или поместите вопрос
(предварительно посмотрев их FAQ) в одну из групп, обсуждающих
определенную операционную систему. Работа с мышью совершенно непохожа
в X window, MS-DOS, Macintosh, и, возможно, в любой другой
операционной системе.
16.5 Как программа может определить полный путь к месту, из которого она
была вызвана?
О: argv[0] может содержать весь путь, часть его или ничего не содержать.
Если имя файла в argv[0] имеется, но информация не полна, возможно
повторение логики поиска исполнимого файла, используемой
интерпретатором командного языка. Гарантированных или мобильных
решений, однако, не существует.
16.6 Как процесс может изменить переменную окружения родительского
процесса?
О: В общем, никак. Различные операционные системы обеспечивают сходную с
UNIX возможность задания пары имя/значение. Может ли программа с
пользой для себя поменять окружение, и если да, то как - все это
зависит от операционной системы.
В системе UNIX процесс может модифицировать свое окружение
(в некоторых системах есть для этого функции setenv() и/или putenv())
и модифицированное окружение обычно передается дочерним процессам
но _не_ распространяется на родительский процесс.
16.7 Как проверить, существует ли файл? Мне необходимо спрашивать
пользователя перед тем как переписывать существующие файлы.
О: В UNIX-подобных операционных системах можно попробовать функцию
access(), хотя имеются кое-какие проблемы. (Применение access() может
сказаться на последующих действиях, кроме того, возможны особенности
исполнения в setuid-программах). Другое (возможно, лучшее) решение
- вызвать stat(), указав имя файла. Единственный универсальный,
гарантирующий мобильность способ состоит в попытке открыть файл.
16.8 Как определить размер файла до его чтения?
О: Если "размер файла" - это количество литер, которое можно прочитать,
то, вообще говоря, это количество заранее неизвестно. В операционной
системе Unix вызов функции stat дает точный ответ, и многие
операционные системы поддерживают похожую функцию, которая дает
приблизительный ответ. Можно c помощью fseek переместиться в конец
файла, а затем вызвать ftell, но такой прием немобилен (дает точный
ответ только в системе Unix, в других же случаях ответ почти точен
лишь для определенных стандартом ANSI "двоичных" файлов).
В некоторых системах имеются подпрограммы filesize или filelength.
И вообще, так ли нужно заранее знать размер файла? Ведь самый точный
способ определения его размера в С программе заключается в открытии и
чтении. Может быть, можно изменить программу так, что размер файла
будет получен в процессе чтения?
16.9 Как укоротить файл без уничтожения или переписывания?
О: В системах BSD есть функция ftruncate(), несколько других систем
поддерживают chsize(), в некоторых имеется (возможно,
недокументированный) параметр fcntl F_FREESP. В системе MS-DOS можно
иногда использовать write(fd, "", 0). Однако, полностью мобильного
решения не существует.
16.10 Как реализовать задержку или определить время реакции пользователя,
чтобы погрешность была меньше секунды?
О: У этой задачи нет, к несчастью, мобильных решений. Unix V7 и ее
производные имели весьма полезную функцию ftime() c точностью до
миллисекунды, но она исчезла в System V и Posix. Поищите такие
функции: nap(), setitimer(), msleep(), usleep(), clock(), и
gettimeofday(). Вызовы select() и poll() (если эти функции доступны)
могут быть добавлены к сервисным функциям для создания простых
задержек. В системе MS-DOS возможно перепрограммирование системного
таймера и прерываний таймера.
16.11 Как прочитать объектный файл и передать управление на одну из его
функций?
О: Необходим динамический компоновщик и/или загрузчик. Возможно выделить
память с помощью malloc и читать объектные файлы, но нужны обширные
познания в форматах объектных файлов, модификации адресов и пр.
В системе BSD Unix можно использовать system() и ld -A для
динамической компоновки. Mногие (большинство? ) версии SunOS и
System V имеют библиотеку -ldl, позволяющую динамически загружать
объектные модули. Есть еще GNU пакет, который называется "dld".
См. также вопрос 7.6.
16.12 Как выполнить из программы команду операционной системы?
О: Используйте system().
Смотри: K&R II Разд. B6 c. 253; ANSI Разд. 4.10.4.5; H&S
Разд. 21.2; PCS Разд. 11 c. 179;
16.13 Как перехватить то, что выдает команда операционной системы?
О: Unix и некоторые другие операционные системы имеют функцию popen(),
которая переназначает поток stdio каналу, связанному с процессом,
запустившим команду, что позволяет прочитать выходные данные (или
передать входные). А можно просто (см. вопрос 16.12) перенаправить
выход команды в файл, затем открыть его и прочесть.
Смотри: PCS Разд. 11 c. 169 .
16.14 Как получить содержимое директория в С программе?
О: Выясните, нельзя ли использовать функции opendir() и readdir(),
доступные в большинстве систем Unix. Реалиции этих функций известны
для MS-DOS, VMS и других систем. (MS-DOS имеет также функции
findfirst и findnext, которые делают в точности то же самое).
16.15 Как работать с последовательными (COM) портами?
О: Это зависит от операционной системы. В системе Unix обычно
осуществляются операции открытия, чтения и записи во внешнее
устройство и используются возможности терминального драйвера для
настройки характеристик. В системе MS-DOS можно либо использовать
прерывания BIOSa, либо (если требуется приличная скорость) один из
управляемых прерываниями пакетов для работы с последовательными
портами.
17. Разное.
17.1 Что можно с уверенностью сказать о начальных значениях переменных,
которые явным образом не инициализированы? Если глобальные переменные
имеют нулевое начальное значение, то правильно ли нулевое значение
присваивается указателям и переменным с плавающей точкой?
А: "Cтатические" переменные (то есть объявленные вне функций и те, что
объявлены как принадлежащие классу stаtic) всегда инициализируются
(прямо при старте программы) нулем, как будто программист написал
"=0". Значит, переменные будут инициализированы как нулевые указатели
(соответствующего типа; см. раздел 1), если они объявлены указателями,
или значениями 0.0, если были объявлены переменные с плавающей точкой.
Переменные автоматического класса (т.е. локальные переменные без
спецификации static), если они явно не определены, первоначально
содержат "мусор". Никаких полезных предсказаний относительно мусора
сделать нельзя.
Память, динамически выделяемая с помощью malloc и realloc также
будет содержать мусор и должна быть инициализирована, если это
необходимо, вызывающей программой. Память, выделенная с помощью
calloc, зануляет все биты, что не всегда годится для указателей или
переменных с плавающей точкой (см. вопрос 3.13 и раздел 1).
17.2 Этот текст взят прямо из книги, но он не компилируется.
f()
{
char a[] = "Hello, world!";
}
О: Возможно, Ваш компилятор создан до принятия стандарта ANSI и
еще не поддерживает инициализацию "автоматических агрегатов"
(то есть нестатических локальных массивов и структур).
Чтобы выкрутиться из этой ситуации, сделайте массив статическим или
глобальным, или инициализируйте его с помощью strcpy, когда
вызывается f(). (Всегда можно инициализировать автоматическую
переменную char * стрингом литер, но см. вопрос 17.20). См. также
вопросы 5.16, 5.17.
17.3 Как писать данные в файл, чтобы их можно было читать на машинах
с другим размером слова, порядком байтов или другим форматом чисел
с плавающей точкой?
О: Лучшее решение - использовать текстовые файлы (обычно ASCII),
c данными, записанными fprintf. Читать данные лучше всего с
помощью fscanf или чего-то подобного. (Такой же совет применим
для сетевых протоколов). К мнениям, что текстовые файлы слишком
велики и могут долго обрабатываться, относитесь скептически.
Помимо того, что эффективность таких операций может быть на практике
приемлемой, способность манипулировать данными с помощью стандартных
средств может иметь решающее значение.
Если необходимо использовать двоичный формат, переносимость данных
можно улучшить (или получить выгоду от использования готовых
библиотек ввода/вывода), если использовать стандартные форматы данных,
такие как XDR (RFC 1014) (Sun), ASN.1(OSI), X.409 (CCITT), или ISO
8825 "Основные правила кодирования" См. также вопрос 9.11.
17.4 Как вставить или удалить строку (или запись) в середине файла?
О: Придется, видимо, переписать файл. См. вопрос 16.9.
17.5 Как возвратить из функции несколько значений?
О: Или передайте указатель на то место, которое будет заполнено функцией,
или пусть функция возвращает структуру, содержащую желаемые значения,
или подумайте о глобальных переменных (если их немного). См. также
вопросы 2.17, 3.4, и 9.2.
17.6 Если есть указатель (char *) на имя функции в виде стринга, то как
эту функцию вызвать?
О: Наиболее прямолинейный путь - создание таблицы имен и соответствующих
им указателей:
int function1(), function2();
struct {char *name; int (*funcptr)(); } symtab[] =
{
"function1", function1,
"function2", function2,
};
Ну а теперь нужно поискать в таблице нужное имя и вызвать функцию,
используя связанный с именем указатель. См. также вопросы 9.9 и 16.11.
17.7 У меня, кажется, нет головного файла <sgtty.h>. Пришлите мне его,
пожалуйста.
О: Стандартные головные файлы существуют в том смысле, что содержат
информацию, необходимую компилятору, операционной системе и
процессору. "Чужой" головной файл подойдет лишь тогда, когда взят из
идентичного окружения. Поинтересуйтесь у продавца компилятора, почему
отсутствует головной файл, или попросите прислать новый взамен
потерянного.
17.8 Как вызвать процедуры, написанные на языке FORTRAN (C++,BASIC,Pascal,
Ada, Lisp) из С (и наоборот).
О: Ответ полностью зависит от машины и от специфики передачи параметров
различными компиляторами. Решения вообще может не быть. Внимательно
читайте руководство по компилятору. Иногда в документации имеется
"Руководство по смешанному программированию", хотя техника передачи
аргументов и обеспечения правильного входа в функцию зачастую весьма
таинственна. Дополнительная информация находится в файле FORT.gz
Глена Гирса, (Glenn Geers) который можно получить с помощью ftp
suphys.physics.su.oz.au в директории src.
Головной файл cfortran.h упрощает взаимодействие C/FORTRAN на многих
популярных машинах. cfortran.h можно получит через ftp
zebra.desy.de (131.169.2.244).
В C++ модификатор "C" внешней функции показывает, что функция будет
вызываться с использованием соглашения о передаче параметров языка
С.
17.9 Кто-нибудь знает о программах, переводящих Pascal или FORTRAN
(или LISP, Ada, awk, "старый" С) в С?
О: Есть несколько общедоступных программ:
p2c Переводчик с Паскаля на С, написанный Дейвом Гиллеспи,
(Dave Gillespie) помещен в comp.sources.unix в Марте 1990
(Volume 21); доступен также через ftp csvax.cs.caltech.edu,
файл pub/p2c-1.20.tar.Z .
ptoc Другой переводчик с Паскаля на С, написан на Паскале
(comp.sources.unix, Volume 10, поправки в vol. 13?)
f2c Переводчик с фортрана на С совместно разработанный
Bell Labs, Bellcore, and Carnegie Mellon. Подробности
можно получить, послав электронной почтой сообщение
"send index from f2c" по адресу netlib@research.att.com
или research!netlib.
(Эти подробности можно получить и через ftp
netlib.att.com, в директории netlib/f2c.)
Составитель этого списка вопросов и ответов имеет список других
коммерческих трансляторов, среди них трансляторы для менее известных
языков. См. также вопрос 5.3.
17.10 Правда ли, что C++ - надмножество С. Можно ли использовать
компилятор C++ для трансляции C программ?
О: С++ вырос из С и в большой степени базируется на нем, но некоторые
правильные конструкции С недопустимы в C++. (Многие С программы,
будут, тем не менее, правильно транслироваться компилятором С++).
17.11 Мне нужен: О: Ищи программы, которые
называются (см. также вопрос 17.12)
генератор перекрестных cflow, calls, cscope
ссылок С
C форматизатор cb, indent
17.12 Где найти все эти общедоступные программы?
О: Если у Вас есть доступ к Usenet, смотрите периодически помещаемые
сообщения в comp.sources.unix и comp.sources.misc, которые описывают
некоторые детали ведения архивов и подсказывают, как получить те
или иные файлы. Обычно используется ftp и/или uucp c центральным,
ориентированным на пользователей сервером, таким как uunet
(ftp.uu.net, 192.48.96.9). Однако, в этих вопросах и ответах
невозможно исследовать или перечислить все архивные серверы и
рассказать о доступе к ним.
Ай Ша (Ajay Shah) поддерживает список общедоступных программ в
области численного анализа, который периодически публикуется,
и его можно найти там же, где и данные вопросы и ответы (см.
вопрос 17.33). Группа Usenet comp.archives содержит многочисленные
объявления о том, что доступно на различных ftp. Почтовый сервер
"archie" может подсказать, на каком ftp имеются те или иные программы.
Пошлите почтовое сообщение "help" по адресу
archie@quiche.cs.mcgill.ca для получения дальнейших инструкций.
Наконец, группа comp.sources.wanted - обычно самое подходящее место,
где можно поместить соответствующий запрос, но посмотрите прежде _их_
список вопросов и ответов (FAQ) "Как найти источники".
17.13 Где состоятся следующие Соревнования по Непонятному С
Программированию (International Obfuscated C Code Contest - IOCCC)?
Как получить программы, победившие в текущем и прошлых конкурсах?
О: Соревнования обычно проходят с начала марта до середины мая. Для
получения правил и рекомендаций, касающихся участия в конкурсе,
пошлите электронной почтой письмо по адресу:
{apple,pyramid,sun,uunet}!hoptoad!judges или
judges@toad.com
,в разделе Subject которого напишите "send rules".
Имейте в виду, что это _не_ адрес, куда нужно посылать конкурсные
программы.
Победители конкурса сначала объявляются на летней конференции
Usenix (Summer Usenix Conference) в середине июня и становятся
доступными в сети где-то в июле-августе. Программы-победители
прошлых конкурсов (начиная с 1984 г.) заархивированы в uunet (см.
вопрос 17.12) в директории ~/pub/ioccc.
В крайнем случае программы-победители прошлых конкурсов можно
получить, послав по указанному выше адресу письмо в графе Subject
которого нужно указать: "send YEAR winners", где YEAR - год,
представленный четырьмя цифрами, интервал в несколько лет или "all".
17.14 Почему недопустимы вложенные комментарии? Как прикажете
"выключить" фрагмент программы, в котором уже есть комментарии?
Можно ли использовать комментарии внутри стринговых констант?
О: Вложенные комментарии принесут больше вреда, чем пользы, главным
образом из-за возможности случайно не закрыть комментарий, оставив
внутри него символы "/*". По этой причине лучше "выключить" большой
фрагмент программы, в котором уже есть комментарии, с помощью средств
препроцессора #ifdef или #if 0 (но имейте в виду вопрос 5.11).
Последовательность символов /* и */ не имеет специального значения
внутри заключенных в двойные кавычки стрингов. Эта последовательность
не рассматривается как комментарий, поскольку программа (особенно
та, которая создает текст другой С програмы) должна иметь
возможность эти комментарии печатать.
Смотри: ANSI Appendix E p. 198, Rationale Разд. 3.1.9 p. 33.
17.15 Как получить значение кода ASCII той или иной литеры, и наоборот?
О: В С литеры представлены целыми числами, соответствующими их значениям.
(в соответствии с набором символов данной машины). Так что нет
необходимости в преобразовании: если известна литера, то известно
и ее значение.
17.16 Как реализовать последовательности и/или массивы бит?
О: Используйте массивы переменных типа char или int и несколько
макросов для операций с отдельными битами (используйте определение
8 для CHAR_BIT, если нет головного файла <limits.h>:
#include <limits.h> /* для CHAR_BIT */
#define BITMASK(bit) (1 << ((bit) % CHAR_BIT))
#define BITSLOT(bit) ((bit) / CHAR_BIT)
#define BITSET(ary, bit) ((ary)[BITSLOT(bit)] |= BITMASK(bit))
#define BITTEST(ary, bit) ((ary)[BITSLOT(bit)] & BITMASK(bit))
17.17 Как наилучшим образом определить число установленных бит,
соответствующих определенному значению?
О: Решение этой и многих других проблем из области битоверчения можно
ускорить и сделать более эффективным с помощью таблиц перекодировки.
(но имейте в виду следующий вопрос).
17.18 Как повысить эффективность работы программы?
О: Тема эффективности, очень часто затрагиваемая в comp.lang.c,
не так важна как многие склонны думать. Большая часть кода в
большинстве программ не влияет на время исполнения. Если время,
занимаемое каким-то участком кода, мало по сравнению с общим
временем исполнения, то для этого участка гораздо важнее простота
и мобильность, чем эффективность. (Помните, что компьютеры очень,
очень быстры и даже "неэффективный" участок кода может выполняться
без видимой задержки).
Печально известны попытки предсказать "горячие точки" программы.
Когда эффективность программы имеет значение, важно использовать
профилировщики для определения тех участков программы, которые
заслуживают внимания. Часто основное время выполнения поглощается
периферийными операциями, такими как ввод/вывод и выделение памяти,
которые можно ускорить с помощью буферизации и хеширования.
Для небольших участков программы, критичных в смысле эффективности,
жизненно важно выбрать подходящий алгоритм; "микрооптимизация"
этого участка менее важна. Многие часто предлагаемые "приемы по
увеличению эффективности" (вроде замены операции сдвига умножением
на степень двойки) выполняются автоматически даже неизощренными
компиляторами.
Неуклюжие попытки оптимизации способны так увеличить размер
программы, что ее эффективность упадет.
Дальнейшее обсуждение противоречий, связанных с эффективностью, а так
же хорошие советы по увеличению эффективности, когда это важно,
смотрите в главе 7 книги Кернигана и Плоджера "Элементы стиля
программирования", а также в книге Джона Бентли "Написание
эффективных программ".
17.19 Правда ли, что применение указателей более эффективно, чем
применение массивов? Насколько замедляет программу вызов функции?
Быстрее ли ++i чем i = i + 1?
О: Точные ответы на эти и многие другие похожие вопросы, конечно же,
зависят от процессора и применяемого компилятора. Если знать это
необходимо, придется аккуратно определить время выполнения тестовых
программ. (Часто различия столь незначительны, что потребуются сотни
тысяч повторений, чтобы их увидеть. Если есть возможность,
посмотрите ассемблерный листинг, выдаваемый компилятором, чтобы
убедиться в различной трансляции двух претендующих на первенство
альтернатив).
"Обычно" быстрее продвигаться по большим массивам с помощью
указателей, чем с помощью индексов, однако есть процессоры, для
которых справедливо обратное.
Хотя вызовы функций и увеличивают время выполнения, сами функции
настолько повышают модульность и простоту понимания программы,
что едва ли полезно от них отказываться.
Прежде чем переписывать выражения типа i=i+1, вспомните, что имеете
дело с компилятором С а не с программируемым калькулятором. Любой
приличный компилятор будет одинаково транслировать ++i,i+=1; i=i+1.
Использовать ++i, i+=1 или i=i+1 - вопрос стиля, не эффективности.
(См. также вопрос 4.7).
17.20 Почему не выполняется такой фрагмент?
char *p = "Hello, world!";
p[0] = tolower(p[0]);
О: Стринговые константы не всегда можно модифицировать, за исключением
случая, когда ими инициализируется массив. Попробуйте
char a[] = "Hello, world!";
(Для компиляции старых программ некоторые компиляторы имеют ключ,
который управляет возможностью модификации стринговых констант.)
См. также вопросы 2.1,2.2, 2.8 и 17.2.
Смотри: ANSI Разд. 3.1.4 .
17.21 Моя программа аварийно завершается еще до выполнения! (если
использовать отладчик, то видно, что смерть наступает еще до
выполнения первой инструкции в main).
О: Видимо, у Вас один или несколько очень больших (более килобайта)
локальных массивов. Во многих системах размер стека фиксирован,
а операционные системы, в которых осуществляется динамическое
выделение стековой памяти, (например, UNIX) могут быть введены в
заблуждение, когда размер стека резко увеличивается.
Часто предпочтительнее объявить большие массивы типа static(если,
конечно, каждый раз при рекурсивном вызове не требуется свежий
массив).
(См. также вопрос 9.4).
17.22 Что означают сообщения "Segmentation violation" и "Bus error" ?
О: Это значит, что программа пытается получить доступ к несуществующей
или запрещенной для нее области памяти. Это постоянно происходит
из-за неинициализированных или неверно инициалированных указателей
(см. вопросы 3.1, 3.2), по вине malloc (см. вопрос 17.23) или, может
быть, scanf (см. вопрос 11.3).
17.23 Моя программа аварийно завершается, очевидно, при выполнении malloc,
но я не вижу в ней ничего плохого.
О: К несчастью, очень легко разрушить внутренние структуры данных,
создаваемые malloc, а возникающие проблемы могут быть трудны для
отладки. Чаще всего проблемы возникают при попытке записать больше
данных, чем может уместиться в памяти, выделенной malloc; особенно
распространена ошибка malloc(strlen(s)) вместо strlen(s) + 1.
Другие проблемы включают освобождение указателей, полученных не
в результате выполнения malloc, или попытки применить функцию
realloc к нулевому указателю. (см. вопрос 3.12).
Существует несколько отладочных пакетов, чтобы помочь отследить
возникающие при применении malloc проблемы. Один из популярных -
"dbmalloc" Конора П. Кахилла, (Conor P. Cahill) помещенный в
comp.sources.misc в сентябре 1992. Другие - это "leak" помещенный в
том 27 архива comp.sources.unix, JMalloc.c и JMalloc.h в сборике
Fidonet Snippets (ищите с помощью archie; см. также вопрос 17.2); и
MEMDEBUG - см. ftp.crpht.lu в pub/sources/memdebug .
См. также вопрос 17.12.
17.24 Есть у кого-нибудь комплект тестов для С компилятора?
О: Плюм Холл (Plum Hall) (ранее работавший в Кардифе, Нью Джерси, теперь
Гаваи) продает такой комплект. Дистрибутив GNU C (gcc) от FSF
включает c-torture-test.tar.Z который выявляет многие проблемы,
возникающие при использованиии компиляторов. Тест Кагана (Kahan) под
названием paranoia, который находится в директории netlib/paranoia на
netlib.att.com интенсивно тестирует операции с плавающей точкой.
17.25 Где достать грамматику С для программы YACC?
О: Самая надежная - конечно же грамматика из стандарта ANSI. Другая
грамматика, подготовленная Джимом Роскиндом (Jim Roskind), находится
на ics.uci.edu в директории pub/*grammar*. Одетый в плоть,
работающий образец ANSI грамматики (принадлежащий Джефу Ли(Jeff Lee))
находится на uunet (см. вопрос 17.12) в директории
usenet/net.sources/ansi.c.grammar.Z (вместе с лексическим
анализатором). В компиляторе GNU C от FSF есть грамматика, так же как
есть она в приложении к книге K&R II.
Смотри: ANSI Разд. A.2 .
17.26 Мне необходим исходный текст для разбора и вычисления формул.
О: Есть два доступных пакета - "defunc" , помещенный в comp.source.misc
в декабре 1993 г. (V41 i32,33), в alt.sources в январе 1994 г.,
его можно получить через ftp sunsite.unc.edu в директории
pub/packages/development/libraries/defunc-1.3.tar.Z; и
пакет "parse" в lamont.ldgo.columbia.edu.
17.27 Мне необходима функция типа strcmp, но для приблизительного
сравнения, чтобы проверить две строки на близость, но не на
тождество.
О: Обычно такие сравнения включают алгоритм "soundex", который
ставит в соответствие сходно звучащим словам один и тот же числовой
код. Этот алгоритм описан в томе "Сортировка и поиск" классической
книги Дональда Кнута "Искусство программирования для ЭВМ".
17.28 Как по дате найти день недели?
О: Используйте mktime (см. вопросы 12.6 и 12.7) или соотношение
Зеллера (Zeller), или попробуйте вот эту функцию, помещенную Томохико
Сакамото (Tomohiko Sakamoto):
dayofweek(y, m, d) /* 0 = Воскресенье */
int y, m, d; /* 1 <= m <= 12, y > 1752 (примерно) */
{
static int t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
y -= m < 3;
return (y + y/4 - y/100 + y/400 + t[m-1] + d) % 7;
}
17.29 2000-й год будет високосным? (год %4 ==0) - правильный тест на
високосный год?
О: Да и нет соответственно. Вот полной тест для Григорианского
календаря:
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
17.30 Как произносить "char"?
О: Ключевое слово С "char" можно произносит тремя способами:
как английские слова "char," "care," or "car;". Выбор за Вами.
17.31 Какие есть хорошие книги для изучения С?
О: Митч Райт (Mitch Wright) поддерживает аннотированную библиографию
книг по С и по UNIX; она доступна через ftp ftp.rahul.net в
директории pub/mitch/YABL.
17.32 Можно ли получить книги по С через Интернет?
О: Можно, по крайней мере, две:
"Notes for C programmers," Кристофера Соутелла,
(Christopher Sawtell) доступна через
svr-ftp.eng.cam.ac.uk:misc/sawtell_C.shar
garbo.uwasa.fi:/pc/c-lang/c-lesson.zip
paris7.jussieu.fr:/contributions/docs
"C for Programmers," Тима Лова (Tim Love)
доступна через
svr-ftp.eng.cam.ac.uk в директории misc.
17.33 Где найти другие варианты этих вопросов и ответов? Доступны
ли более ранние редакции?
О: Пошарьте по Сети. Обычно эти вопросы и ответы помещаются в
comp.lang.c первого числа каждого месяца, со значением поля Expires,
позволяющим присутствовать в comp.lang.c весь месяц. Там же
есть сокращенная версия, представляющая собой список изменений,
сопровождающий существенно обновленную версию. Такие списки можно
так же найти в comp.answers и news.answers. Несколько серверов ведут
архивы сообщений, помещаемых в news.answers, а также списков часто
задаваемых вопросов (FAQ), включая и этот. Вот два сервера
rtfm.mit.edu (директории pub/usenet/news.answers/C-faq/ и
pub/usenet/comp.lang.c/ ) и ftp.uu.net (директорий
usenet/news.answers/C-faq/ ). Сервер archie должен помочь найти
другие архивы; сделайти запрос на "prog C-faq". Дополнительную
информацию можно найти в списке meta-FAQ в news.answers; см. также
вопрос 17.12.
Этот документ - постоянныо расширяющийся список вопросов, которые
часто задавались со времен Великого Переименования, а не коллекция
вопросов, которые были интересны в этом месяце. Предыдущие версии
этого документа устарели, и в них нет ничего интересного за
исключением случайных орфографических ошибок, которых в данной версии
уже нет.
Библиграфия
ANSI American National Standard for Information Systems --
Programming Language -- C, ANSI X3.159-1989 (см. вопрос 5.2).
JLB Jon Louis Bentley, Writing Efficient Programs, Prentice-Hall,
1982, ISBN 0-13-970244-X.
H&S Samuel P. Harbison and Guy L. Steele, C: A Reference Manual,
Second Edition, Prentice-Hall, 1987, ISBN 0-13-109802-0.
(скоро появится третье издание.)
PCS Mark R. Horton, Portable C Software, Prentice Hall, 1990,
ISBN 0-13-868050-7.
EoPS Brian W. Kernighan and P.J. Plauger, The Elements of Programming
Style, Second Edition, McGraw-Hill, 1978, ISBN 0-07-034207-5.
Русский перевод: Б.Керниган,Ф.Плоджер "Элементы стиля
программирования", Радио и связь, 1984, 160 стр.
K&R I Brian W. Kernighan and Dennis M. Ritchie, The C Programming
Language, Prentice-Hall, 1978, ISBN 0-13-110163-3.
Русский перевод: Керниган Б., Ритчи Д. "Язык программирования
С", М.: Финансы и статистика,1985.
K&R II Brian W. Kernighan and Dennis M. Ritchie, The C Programming
Language, Second Edition, Prentice Hall, 1988, ISBN 0-13-
110362-8, 0-13-110370-9.
Русский перевод:Б.Керниган, Д.Ритчи "Язык программирования С",
М.: Финансы и статистика, 1992.
Knuth Donald E. Knuth, The Art of Computer Programming, (3 vols.),
Addison-Wesley, 1981.
Русский перевод: Д.Кнут "Искусство программирования для ЭВМ",
т.1 Основные алгоритмы (М.:Мир,1976),т.2 Получисленные алгоритмы
(М.:Мир,1977), т.3 "Сортировка и поиск", (М.:Мир, 1978)
CT&P Andrew Koenig, C Traps and Pitfalls, Addison-Wesley, 1989,
ISBN 0-201-17928-8.
P.J. Plauger, The Standard C Library, Prentice Hall, 1992,
ISBN 0-13-131509-9.
Harry Rabinowitz and Chaim Schaap, Portable C, Prentice-Hall,
1990, ISBN 0-13-685967-4.
Более подробную библиографию можно найти в переработанном руководстве
по стилю Indian Hill (см. вопрос 14.3, а также вопрос 17.31).
Благодарности
Спасибо следующим людям:
Jamshid Afshar, Sudheer Apte, Randall Atkinson, Dan Bernstein,
Vincent Broman, Stan Brown, Joe Buehler, Gordon Burditt, Burkhard Burow,
Conor P. Cahill, D'Arcy J.M. Cain, Christopher Calabrese, Ian Cargill,
Paul Carter, Billy Chambless, Raymond Chen, Jonathan Coxhead, Lee
Crawford, Steve Dahmer, Andrew Daviel, James Davies, Jutta Degener, Norm
Diamond, Jeff Dunlop, Ray Dunn, Stephen M. Dunn, Michael J. Eager, Dave
Eisen, Bjorn Engsig, Chris Flatters, Rod Flores, Alexander Forst, Jeff
Francis, Dave Gillespie, Samuel Goldstein, Alasdair Grant, Ron
Guilmette, Doug Gwyn, Tony Hansen, Joe Harrington, Guy Harris, Elliotte
Rusty Harold, Jos Horsmeier, Blair Houghton, Ke Jin, Kirk Johnson, Larry
Jones, Kin-ichi Kitano, Peter Klausler, Andrew Koenig, Tom Koenig, Ajoy
Krishnan T, Markus Kuhn, John Lauro, Felix Lee, Mike Lee, Timothy J.
Lee, Tony Lee, Don Libes, Christopher Lott, Tim Love, Tim McDaniel,
Stuart MacMartin, John R. MacMillan, Bob Makowski, Evan Manning, Barry
Margolin, George Matas, Brad Mears, Bill Mitchell, Mark Moraes, Darren
Morby, Ken Nakata, Landon Curt Noll, David O'Brien, Richard A. O'Keefe,
Hans Olsson, Philip (lijnzaad@embl-heidelberg.de), Andrew Phillips,
Christopher Phillips, Francois Pinard, Dan Pop, Kevin D. Quitt, Pat
Rankin, J. M. Rosenstock, Erkki Ruohtula, Tomohiko Sakamoto, Rich Salz,
Chip Salzenberg, Paul Sand, DaviD W. Sanderson, Christopher Sawtell,
Paul Schlyter, Doug Schmidt, Rene Schmit, Russell Schulz, Patricia
Shanahan, Peter da Silva, Joshua Simons, Henry Spencer, David Spuler,
Melanie Summit, Erik Talvola, Clarke Thatcher, Wayne Throop, Chris
Torek, Andrew Tucker, Goran Uddeborg, Rodrigo Vanegas, Jim Van Zandt,
Wietse Venema, Ed Vielmetti, Larry Virden, Chris Volpe, Mark Warren,
Larry Weiss, Freek Wiedijk, Lars Wirzenius, Dave Wolverton, Mitch
Wright, Conway Yee, и Zhuo Zang, кто явно или неявно внес вклад в этот
документ.
Отдельно хочу поблагодарить Карла Хойера (Karl Heuer) и в особенности
Марка Бредера (Mark Brader), кто, говоря словами Стива Джонсона,
(Steve Johnson) "в безжалостном стремлении улучшить этот документ
уводил меня не только за пределы моих желаний, но и за пределы
моих возможностей".
Стив Саммит (Steve Summit)
Авторские права на этот документ принадлежат Стиву Саммиту (Steve Summit).
Документ можно свободно распространять пока в нем
присутствует имя автора и эти строки. Отрывки С программ в этом документе
(vstrcat(), error(), и т.д.) относятся к общедоступным и могут использоваться
без ограничений.
Послесловие переводчика
Отдельные фрагменты "Вопросов и ответов по языку С" я начал
переводить "для себя", но логика работы заставила меня перевести весь
документ от начала до конца.
Комментарии |
отсутствуют |
Добавление комментария |