Для всех, кто задавался вопросами о странностях языков Си и C++. Must read!
Несмотря на то, что год издания — 1995. Книга актуальна, потому что количество граблей — монотонно возрастающая функция
Главы оформлены как ветки этой темы. Разворачивайте и смотрите.
Источник: книга Стива Саммита C Frequently Asked Questions и ее перевод Язык Си в вопросах и ответах.
(нумерация глав в оригинале и переводе различна; здесь воспроизведен перевод)
(c) Steve Summit, 1995. E-mail:
(c) Крупник А.Б. Перевод с английского, 1996. E:mail:
ПРЕДИСЛОВИЕ
Некоторые вопросы появляются вновь и вновь в этой конференции. Это хорошие вопросы, и ответы на них могут быть далеко не очевидны, но каждый раз ресурсы Сети и время читателя тратятся на повторяющиеся отклики и на нудные поправки к некорректным ответам, возникновение которых неизбежно.
В этом документе, публикуемом ежемесячно, делается попытка ответить на такие вопросы ясно и кратко, чтобы обсуждения стали более плодотворными, а не возвращались постоянно к основным принципам.
Никакой список вопросов и ответов не заменит тщательного изучения хорошего учебника или справочника по языку С. Тому, кто с интересом участвует в этой конференции, должно быть также интересно прочесть одну или несколько таких книг, желательно не один раз. Качество некоторых книг и руководств по компилятору нельзя, к сожалению, назвать высоким; есть в них и попытки увековечить некоторые мифы, которые данный документ пытается развеять.
Несколько заслуживающих внимания книг по С перечислены в библиографии. Многие вопросы и ответы содержат ссылки на эти книги для дальнейшего изучения интересующимся и увлеченным читателем. (Но помните о различной нумерации документов ANSI и ISO стандартов С; см. вопрос 5.1).
Если у Вас есть вопрос, касающийся языка С, на который нет ответа в данном документе, попытайтесь получить ответ на него из перечисленных здесь книг, или спросите у знающих коллег, прежде чем использовать Сеть. Многие будут счастливы ответить на ваши вопросы, но количество повторяющихся ответов на один и тот же вопрос, как и возрастающее по мере привлечения читателей число вопросов, может стать угнетающим. Если у Вас есть вопросы или замечания по этому документу, используйте, пожалуйста, электронную почту -- эти вопросы и ответы призваны снизить нагрузку на Сеть, а не увеличить ее.
Кроме списка наиболее часто задаваемых вопросов, в этом документе суммированы наиболее часто даваемые ответы. Даже если Вы знаток языка С, полезно найти время, чтобы бегло просмотреть этот документ, и тогда Вам не придется зря тратить время в случае, когда кто-то поместил вопрос, ответ на который уже дан.
Этот документ был в последний раз изменен 1 апреля 1995 года, и за время путешествия по Сети мог сильно удалиться от своего источника — сети USENET. В данный момент он, возможно, устарел, особенно если Вы смотрите твердую копию или файл, загруженный с какого-нибудь заштатного сервера или списанный с CD-ROM. Всегда есть возможность получить последнюю редакцию этого файла
через ftp ftp.eskimo.com, rtfm.mit.edu, or ftp.uu.net (см. вопросы 17.12 и 17.33), или, послав электронной почтой сообщение "help" по адресу . (Имейте также в виду, что этот документ предназначен для свободного распространения. Вы не должны платить за него).
Можно получить и другие версии этого документа. Вместе с ним помещается сокращенная версия и (когда были изменения) список различий между текущей и предыдущей версией. Несколько предварительных гипертектовых версий документа доступны через world-wide web (WWW). Смотрите http://www.cis.ohio-state.edu/hypertext/faq/usenet/FAQ-List.html и http://www.lysator.liu.se/c/c-faq/index.html. Наконец, для тех, кто предпочитает книгу в переплете (и более подробные ответы на еще большее число вопросов!), издательство Addison-Wesley осенью 1995 опубликует книжную версию этих вопросов и ответов (ISBN 0-201-84519-9).
Работа над этим документом идет постоянно. Желательно и Ваше участие.
Комментарии направляйте по адресу scs@eskimo.com . Стив Саммит
...И, естественно, в RSDN.ru. Николай Меркин == Кодт
Вопрос: Если "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.
Перекуём баги на фичи!
2.18. Как инкрементировать char*, словно он указывает на int
Вопрос: У меня определен указатель на 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.
Примечание:
Ещё можно сделать то же самое через указатель на указатель
(*(int**)(&p))++;
поскольку результат разыменования — это lvalue, и к нему применимы присваивание и инкремент. -- Кодт
еще не готово
3. Выделение памяти
4. Выражения
5. ANSI C
6. Препроцессор С
7. Списки аргументов переменной длины
8. Булевы выражения и переменные
9. Структуры, перечисления и объединения
10. Декларации
11. Cтандартный ввод/вывод
12. Библиотечные функции
13. Lint
14. Стиль
15. Операции с плавающей точкой
16. Интерфейс с операционной системой
17. Разное (Пребразование Fortran -> C , грамматики для YACC и т.п.)
Эта глава обсуждает внутреннюю реализацию нулевых указателей. Для большинства, наверное, она имеет лишь академический характер, так сказать, для расширения кругозора (поскольку на популярных платформах NULL — это 0). Тем не менее, если вы натолкнулись на "странности" с нулевыми указателями — прочтите.
Здесь, естественно, не освещены вопросы абстрактных методов (синтаксис, да и смысл которых также использует нулевой указатель: virtual void ToDo() = 0; Это — особый повод для разбирательств, отчасти освещенный в вопросах 1.1
Итак, представьте себе разговор немного паникующего новичка и спокойного "гуру"
ВОПРОСЫ
1.1. Расскажите все-таки о пресловутых нулевых указателях.
1.2. Как "получить" нулевой указатель в программе?
1.3. Что такое NULL и как он определен с помощью #define?
1.4. Как #define должен определять NULL на машинах, использующих ненулевой двоичный код для внутреннего представления нулевого указателя?
1.5. Можно ли передавать функциям NULL как ((char*)0) без преобразования типа?
1.6. Я использую макрос #define Nullptr(type) (type *)0 ...
1.7. Корректно ли использовать сокращенный условный оператор if(p) для проверки того, что указатель ненулевой? А что если внутреннее представление для нулевых указателей отлично от нуля?
1.8. Если "NULL" и "0" эквивалентны, то какую форму из двух использовать?
1.9. Но не лучше ли будет использовать NULL (вместо 0) в случае, когда значение NULL изменяется, быть может, на компьютере с ненулевым внутренним представлением нулевых указателей?
1.10. Я в растерянности. Гарантируется, что NULL равен 0, а нулевой указатель нет?
1.11. Почему так много путаницы связано с нулевыми указателями? Почему так часто возникают вопросы?
1.12. Я все еще в замешательстве. Мне так и не понятна возня с нулевыми указателями.
1.13. Учитывая всю эту путаницу, связанную с нулевыми указателями, не лучше ли просто потребовать, чтобы их внутреннее представление было нулевым?
1.14. Ну а если честно, на какой-нибудь реальной машине используются ненулевые внутренние представления нулевых указателей или разные представления для указателей разных типов?
1.15. Что означает ошибка во время исполнения "null pointer assignment" (запись по нулевому адресу). Как мне ее отследить?
Вопрос: Расскажите все-таки о пресловутых нулевых указателях.
Ответ:
Для каждого типа указателей существует (согласно определению языка) особое значение — "нулевой указатель", которое отлично от всех других значений и не указывает на какой-либо объект или функцию. Таким образом, ни оператор &, ни успешный вызов 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.
Вопрос: Как "получить" нулевой указатель в программе?
Ответ:
В языке С константа 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 .
Вопрос: Что такое NULL и как он определен с помощью #define?
Ответ:
Многим программистам не нравятся нули, беспорядочно разбросанные по программам.
По этой причине макрос препроцессора NULL определен в <stdio.h> или <stddef.h> как значение 0 (или (void *) 0, об этом значении поговорим позже.)
Программист, который хочет явно различать 0 как целое и 0 как нулевой указатель может использовать NULL в тех местах, где необходим нулевой указатель. Это только стилистическое соглашение; препроцессор преобразует NULL опять в 0, который затем распознается компилятором в соответствующем контексте как нулевой указатель. В отдельных случаях при передаче параметров функции, может все же потребоваться явное указание типа перед NULL (как и перед 0). (Таблица в вопросе 1.2
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.
Вопрос: Как #define должен определять NULL на машинах, использующих ненулевой двоичный код для внутреннего представления нулевого указателя?
Ответ:
Программистам нет необходимости знать внутреннее представление(я) нулевых указателей, ведь об этом обычно заботится компилятор. Если машина использует ненулевой код для представления нулевых указателей, на совести компилятора генерировать этот код, когда программист обозначает нулевой указатель как "0" или NULL.
Следовательно, определение NULL как 0 на машине, для которой нулевые указатели представляются ненулевыми значениями, так же правомерно, как и на любой другой, так как компилятор должен (и может) генерировать корректные значения нулевых указателей в ответ на 0, встретившийся в соответствующем контексте.
Вопрос: Пусть NULL был определен следующим образом:
#define NULL ((char *)0)
Ознает ли это, что функциям можно передавать NULL без преобразования типа?
Ответ:
В общем, нет. Проблема в том, что существуют компьютеры, которые используют различные внутренние представления для указателей на различные типы данных. Предложенное определение через #define годится, когда функция ожидает в качестве передаваемого параметра указатель на char, но могут возникнуть проблемы при передаче указателей на переменные других типов, а верная конструкция
FILE *fp = NULL;
может не сработать.
Тем не менее, ANSI C допускает другое определение для NULL:
#define NULL ((void *)0)
Кроме помощи в работе некорректным программам (но только в случае машин, где указатели на разные типы имеют одинаковые размеры, так что помощь здесь сомнительна) это определение может выявить программы, которые неверно используют NULL (например, когда был необходим символ ASCII NUL; см. вопрос 1.8).
Ответ:
Хотя этот трюк и популярен в определенных кругах, он стоит немного.
Он не нужен при сравнении и присваивании; см. вопрос 1.2. Он даже не экономит буквы. Его использование показывает тому, кто читает программу, что автор здорово "сечет" в нулевых указателях, и требует гораздо более аккуратной проверки определения макроса, его использования и всех остальных случаев применения указателей.
Вопрос: Корректно ли использовать сокращенный условный оператор 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.
Вопрос: Но не лучше ли будет использовать NULL (вместо 0) в случае, когда значение NULL изменяется, быть может, на компьютере с ненулевым внутренним представлением нулевых указателей?
Ответ:
Нет. Хотя символические константы часто используются вместо чисел из-за того, что числа могут измениться, в данном случае причина, по которой используется NULL, иная. Еще раз повторим: язык гарантирует, что 0, встреченный там, где по контексту подразумевается указатель, будет заменен компилятором на нулевой указатель.
NULL используется только с точки зрения лучшего стиля программирования.
Он представляется с помощью...
Внутреннее (на стадии выполнения) представление нулевого указателя, которое может быть отлично от нуля и различаться для различных типов указателей. О внутреннем представлении нулевого указателя должны заботиться только создатели компилятора.
Программистам на С это представление не известно, поскольку они используют... Синтаксическое соглашение для нулевых указателей, символ "0".
Вместо него часто используют... Макрос NULL который с помощью #define определен как "0" или "(void *)0".
Наконец, нас может запутать...
Нулевой код ASCII (NUL), в котором все биты равны нулю, но который имеет мало общего с нулевым указателем, разве что названия похожи;
и...
"Нулевой стринг", или, что то же самое, пустой стринг (""). Термин "нулевой стринг" может приводить к путанице в С и, возможно, его следует избегать, так как пустой стринг включает символ '\0', но не нулевой указатель, и здесь мы уже идем по кругу...
В этом документе фраза "нулевой указатель" (null прописными буквами) используется в смысле 1, символ "0" в смысле 3, а слово "NULL", записанное большими буквами, в смысле 4.
Перекуём баги на фичи!
1.11. Почему так много путаницы с нулевыми указателями?
Вопрос: Почему так много путаницы связано с нулевыми указателями? Почему так часто возникают вопросы?
Ответ:
Программисты на С традиционно хотят знать больше, чем это необходимо для программирования, о внутреннем представлении кода.
Тот факт, что внутреннее представление нулевых указателей для большинства машин совпадает с их представлением в исходном тексте, т.е. нулем, способствует появлению неверных обобщений.
Использование макроса (NULL) предполагает, что значение может впоследствии измениться, или иметь другое значение для какого-нибудь компьютера.
Конструкция "if(p == 0)" может быть истолкована неверно, как преобразование перед сравнением p к целому типу, а не 0 к типу указателя.
Наконец, часто не замечают, что термин "null" (ноль, нулевой) употребляется в разных смыслах (перечисленных выше, см. вопрос 1.10
Хороший способ устранить путаницу — вообразить, что язык С имеет ключевое слово (возможно, nil, как в Паскале), которое обозначает нулевой указатель. Компилятор либо пребразует "nil" в нулевой указатель нужного типа, либо сообщает об ошибке, когда этого сделать нельзя. На самом деле, ключевое слово для нулевого указателя в С — это не "nil" а "0". Это ключевое слово работает всегда, за исключением случая, когда компилятор воспринимает в неподходящем контексте "0" без указания типа как целое число, равное нулю, вместо того, чтобы сообщить об ошибке. Программа может не работать, если предполагалось, что "0" без явного указания типа — это нулевой указатель.
Перекуём баги на фичи!
1.12. Мне так и не понятна возня с нулевыми указателями
Вопрос: Я все еще в замешательстве. Мне так и не понятна возня с нулевыми указателями.
Ответ:
Следуйте двум простым правилам:
1. Для обозначения в исходном тексте нулевого указателя, используйте "0" или "NULL".
2. Если "0" или "NULL" используются как фактические аргументы при вызове функции, приведите их к типу указателя, который ожидает вызываемая функция.
Остальная часть дискуссии посвящена другим заблуждениям, связанным с нулевыми указателями, внутреннему представлению нулевых указателей (которое Вам знать не обязательно), а также усовершенствованиям стандарта ANSI C.
Вопрос: Учитывая всю эту путаницу, связанную с нулевыми указателями, не лучше ли просто потребовать, чтобы их внутреннее представление было нулевым?
Ответ:
Если причина только в этом, то поступать так было бы неразумно, так как это неоправданно ограничит конкретную реализацию, которая (без таких ограничений) будет естественным образом представлять нулевые указатели специальными, отличными от нуля значениями, особенно когда эти значения автоматически будут вызывать специальные аппаратные прерывания, связанные с неверным доступом.
Кроме того, что это требование даст на практике?
Понимание нулевых указателей не требует знаний о том, нулевое или ненулевое их внутреннее представление.
Предположение о том, что внутреннее представление нулевое, не приводит к упрощению кода (за исключением некоторых случаем сомнительного использования calloc; см. вопрос 3.13).
Знание того, что внутреннее представление равно нулю, не упростит вызовы функций, так как размер указателя может быть отличным от размера указателя на int.
(Если вместо "0" для обозначения нулевого указателя использовать "nil" (см. вопрос 1.11
Вопрос: Ну а если честно, на какой-нибудь реальной машине используются ненулевые внутренние представления нулевых указателей или разные представления для указателей разных типов?
Ответ:
Серия 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> (вообще говоря, несуществующий хендл <объект, смещение>) как нулевой указатель С.
В зависимости от модели памяти, процессоры 80x86 (IBM PC) могут использовать либо 16-битные указатели на данные и 32-битные указатели на функции, либо, наоборот, 32-битные указатели на данные и 16-битные — на функции.
Старые модели HP 3000 используют различные схемы адресации для байтов и для слов. Указатели на char и на void, имеют, следовательно, другое представление, чем указатели на int (на структуры и т.п.), даже если адрес одинаков.
Вопрос: Что означает ошибка во время исполнения "null pointer assignment" (запись по нулевому адресу). Как мне ее отследить?
Ответ:
Это сообщение появляется только в системе MS-DOS (и в DOS-сессиях под Windows -- Кодт) (см., следовательно, раздел 16) и означает, что произошла запись либо с помощью неинициализированного, либо нулевого указателя в нулевую область.
Отладчик обычно позволяет установить точку останова при доступе к нулевой области. Если это сделать нельзя, Вы можете скопировать около 20 байт из области 0 в другую и периодически проверять, не изменились ли эти данные.
Вопрос: В одном файле у меня есть описание char a[6], а в другом я объявил extern char *a. Почему это не работает?
Ответ:
Декларация extern char *a просто не совпадает с текущим определением.
Тип "Указатель-на-тип-Т" не равен типу "массив-типа-Т". Используйте extern char a[].
Вопрос: Но я слышал, что 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. Взаимозаменяемость в качестве формальных параметров
Вопрос: Тогда почему объявления указателей и массивов взаимозаменяемы в качестве формальных параметров?
Ответ:
Оператор sizeof сообщает размер указателя, который на самом деле
получает функция. (см. вопрос 2.4).
Так как массивы немедленно превращаются в указатели, массив на самом деле не передается в функцию. По общему правилу, любое похожее на массив объявление параметра
f(char a[])
рассматривается компилятором как указатель, так что если был передан массив, функция получит:
f(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.2
Вопрос: C практической точки зрения в чем разница между массивами и указателями?
Ответ:
Массивы автоматически резервируют память, но не могут изменить расположение в памяти и размер.
Указатель должен быть задан так, чтобы явно указывать на выбранный участок памяти (возможно с помощью malloc), но он может быть по нашему желанию переопределен (т.е. будет указывать на другие объекты) и, кроме того, указатель имеет много других применений, кроме службы в качестве базового адреса блоков памяти.
В рамках так называемой эквивалентности массивов и указателей (см. вопрос 2.3
), массивы и указатели часто оказываются взаимозаменяемыми. Особенно это касается блока памяти, выделенного функцией malloc, указатель на который часто используется как настоящий массив. (На этот блок памяти можно ссылаться, используя оператор [], cм. вопрос 2.14, а также вопрос 17.20.)
Вопрос: Я наткнулась на шуточный код, содержащий "выражение" 5["abcdef"].
Почему такие выражения возможны в С?
Ответ:
Да, Вирджиния, индекс и имя массива можно переставлять в С.
Этот забавный факт следует из определения индексации через указатель, а именно, a[e] идентично *((a)+(e)), для любого выражения е и основного выражения а, до тех пор пока одно из них будет указателем, а другое целочисленным выражением.
Эта неожиданная коммутативность часто со странной гордостью упоминается в С-текстах, но за пределами
Соревнований по Непонятному Программированию (Obfuscated C Contest) она применения не находит. (см. вопрос 17.13).
Смотри: ANSI Rationale Разд. 3.3.2.1 c. 41.
Перекуём баги на фичи!
2.10. Ошибка компиляции при передаче 2-мерного массива
Вопрос: Мой компилятор ругается, когда я передаю двумерный массив функции, ожидающей указатель на указатель.
Ответ:
Правило, по которому массивы превращаются в указатели, не может применяться рекурсивно. Массив массивов (т.е. двумерный массив в С) превращается в указатель на массив, а не в указатель на указатель.
Указатели на массивы могут вводить в заблуждение и применять их нужно с осторожностью. (Путаница еще более усугубляется тем, что существуют некорректные компиляторы, включая некоторые версии 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. Как передать 2-мерный массив неизвестного размера?
Вопрос: Как писать функции, принимающие в качестве параметра двумерные массивы, "ширина" которых во время компиляции неизвестна?
Ответ:
Это непросто. Один из путей — передать указатель на элемент [0][0] вместе с размерами и затем симулировать индексацию "вручную":
f2(int* aryp, int nrows, int 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 разрешает объявлять локальные массивы, которые имеют размеры, задаваемые аргументами функции, но это — нестандартное расширение.
Обычно этого делать не нужно. Когда случайно говорят об указателе на массив, обычно имеют в виду указатель на первый элемент массива.
Вместо указателя на массив рассмотрим использование указателя на один из элементов массива.
Массивы типа T превращаются в указатели типа Т (см. вопрос 2.3
), что удобно; индексация или увеличение указателя позволяет иметь доступ к отдельным элементам массива.
Истинные указатели на массивы при увеличении или индексации указывают на следующий массив и в общем случае если и полезны, то лишь при операциях с массивами массивов. (Cм. вопрос 2.10
Если действительно нужно объявить указатель на целый массив, используйте что-то вроде
int (*ap)[N];
где N — размер массива.
(Cм. также вопрос 10.4.)
Если размер массива неизвестен, параметр N может быть опущен, но получившийся в результате тип "указатель
на массив неизвестного размера" — бесполезен.
).
В языке C до выхода стандарта ANSI оператор & в &array игнорировался, порождая предупреждение компилятора. Все компиляторы C, встречая просто имя массива, порождают указатель типа указатель-на-Т, т.е. на первый элемент массива. (Cм. также вопрос 2.3
Вопрос: Как динамически выделить память для многомерного массива?
Ответ:
Лучше всего выделить память для массива указателей, а затем инициализировать каждый указатель так, чтобы он указывал на динамически создаваемую строку. Вот пример для двумерного массива:
(В "реальной" программе, 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. Равноправное использование стат. и дин. массивов
Следующие два вызова, возможно, будут работать, но они включают сомнительные приведения типов, и работают лишь в том случае, когда динамически задаваемое число столбцов ncolumns совпадает с NCOLUMS:
Если Вы способны понять, почему все вышеперечисленные вызовы работают и написаны именно так, а не иначе, и если Вы понимаете, почему сочетания, не попавшие в список, работать не будут, то у Вас очень хорошее понимание массивов и указателей (и нескольких других областей) C.
Перекуём баги на фичи!
2.16. Трюк: массив, индекс которого начинается не с 0
то теперь можно рассматривать "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.
Вопрос: Я передаю функции указатель, который она инициализирует
...
int *ip;
f(ip);
...
void f(ip)
int *ip;
{
static int dummy = 5;
ip = &dummy;
}
но указатель после вызова функции остается неизменным.
Ответ:
Функция пытается изменить сам указатель, или то, на что он ссылается?
Помните, что аргументы в С передаются по значению. Вызываемая функция изменяет только копию передаваемого указателя. Вам нужно либо передать адрес указателя (функцию будет в этом случае принимать указатель на указатель), либо сделать так, чтобы функция возвращала указатель.
Примечание:
out-параметры (параметры, изменяемые функцией) на C передаются по указателю:
void f(ip_ptr, i_ptr)
int **ip_ptr; /* out-параметр - указатель на статическую переменную */int *i_ptr; /* out-параметр - целое число */
{
assert(ip_ptr != NULL); /* Передан указатель на реально существующий приёмник типа int*. */
assert(i_ptr != NULL);
static int dummy = 5;
/* присвоили значения приёмникам */
*ip_ptr = &dummy;
*i_ptr = rand();
}
...
int *ip;
int x;
f(&ip, &x);
В языке С++ можно передавать параметры по ссылке:
void f(int* &ip, int &i)
{
static int dummy = 5;
ip = &dummy;
i = 123;
}
...
int *ip;
int i;
f(ip, i);