Страница:
Как видим, все "запоминающие" переменные (т.е. prevchar) вынесены из самой функции и
подаются в нее в виде аргумента.
Реентерабельные функции независимы от остальной части программы (их можно скопи-
ровать в другой программный проект без изменений), более понятны (поскольку все зат-
рагиваемые ими внешние переменные перечислены как аргументы, не надо выискивать в
теле функции глобальных переменных, передающих значение в/из функции, т.е. эта функ-
ция не имеет побочных влияний), более надежны (хотя бы потому, что компилятор в сос-
тоянии проверить прототип такой функции и предупредить вас, если вы забыли задать
какой-то аргумент; если же аргументы передаются через глобальные переменные - вы
можете забыть проинициализировать какую-то из них). Старайтесь делать функции реен-
терабельными!
А. Богатырев, 1992-95 - 75 - Си в UNIX
Вот еще один пример на эту тему. Не-реентерабельный вариант:
int x, y, result;
int f (){
static int z = 4;
y = x + z; z = y - 1;
return x/2;
}
Вызов: x=13; result = f(); printf("%d\n", y);
А вот реентерабельный эквивалент:
int y, result, zmem = 4;
int f (/*IN*/ int x, /*OUT*/ int *ay, /*INOUT*/ int *az){
*az = (*ay = x + *az) - 1;
return x/2;
}
Вызов: result = f(13, &y, &zmem); printf("%d\n", y);
1.145. То, что формат заголовка функции должен быть известен компилятору до момента
ее использования, побуждает нас помещать определение функции до точки ее вызова. Так,
если main вызывает f, а f вызывает g, то в файле функции расположатся в порядке
g() { }
f() { ... g(); ... }
main(){ ... f(); ... }
Программа обычно разрабатывается "сверху-вниз" - от main к деталям. Си же вынуждает
нас размещать функции в программе в обратном порядке, и в итоге программа читается
снизу-вверх - от деталей к main, и читать ее следует от конца файла к началу!
Так мы вынуждены писать, чтобы удовлетворить Си-компилятор:
#include <stdio.h>
unsigned long g(unsigned char *s){
const int BITS = (sizeof(long) * 8);
unsigned long sum = 0;
for(;*s; s++){
sum ^= *s;
/* cyclic rotate left */
sum = (sum<<1)|(sum>>(BITS-1));
}
return sum;
}
void f(char *s){
printf("%s %lu\n", s, g((unsigned char *)s));
}
int main(int ac, char *av[]){
int i;
for(i=1; i < ac; i++)
f(av[i]);
return 0;
}
А вот как мы разрабатываем программу:
А. Богатырев, 1992-95 - 76 - Си в UNIX
#include <stdio.h>
int main(int ac, char *av[]){
int i;
for(i=1; i < ac; i++)
f(av[i]);
return 0;
}
void f(char *s){
printf("%s %lu\n", s, g((unsigned char *)s));
}
unsigned long g(unsigned char *s){
const int BITS = (sizeof(long) * 8);
unsigned long sum = 0;
for(;*s; s++){
sum ^= *s;
/* cyclic rotate left */
sum = (sum<<1)|(sum>>(BITS-1));
}
return sum;
}
и вот какую ругань производит Си-компилятор в ответ на эту программу:
"0000.c", line 10: identifier redeclared: f
current : function(pointer to char) returning void
previous: function() returning int : "0000.c", line 7
"0000.c", line 13: identifier redeclared: g
current : function(pointer to uchar) returning ulong
previous: function() returning int : "0000.c", line 11
Решением проблемы является - задать прототипы (объявления заголовков) всех функций в
начале файла (или даже вынести их в header-файл).
#include <stdio.h>
int main(int ac, char *av[]);
void f(char *s);
unsigned long g(unsigned char *s);
...
Тогда функции будет можно располагать в тексте в любом порядке.
1.146. Рассмотрим процесс сборки программы из нескольких файлов на языке Си. Пусть
мы имеем файлы file1.c, file2.c, file3.c (один из них должен содержать среди других
функций функцию main). Ключ компилятора -o заставляет создавать выполняемую прог-
рамму с именем, указанным после этого ключа. Если этот ключ не задан - будет создан
выполняемый файл a.out
cc file1.c file2.c file3.c -o file
Мы получили выполняемую программу file. Это эквивалентно 4-м командам:
cc -c file1.c получится file1.o
cc -c file2.c file2.o
cc -c file3.c file3.o
cc file1.o file2.o file3.o -o file
Ключ -c заставляет компилятор превратить файл на языке Си в "объектный" файл
А. Богатырев, 1992-95 - 77 - Си в UNIX
(содержащий машинные команды; не будем вдаваться в подробности). Четвертая команда
"склеивает" объектные файлы в единое целое - выполняемую программу|-. При этом, если
какие-то функции, используемые в нашей программе, не были определены (т.е. спрограм-
мированы нами) ни в одном из наших файлов - будет просмотрена библиотека стандартных
функций. Если же каких-то функций не окажется и там - будет выдано сообщение об
ошибке.
Если у нас уже есть какие-то готовые объектные файлы, мы можем транслировать
только новые Си-файлы:
cc -c file4.c
cc file1.o file2.o file3.o file4.o -o file
или (что то же самое,
но cc сам разберется, что надо делать)
cc file1.o file2.o file3.o file4.c -o file
Существующие у нас объектные файлы с отлаженными функциями удобно собрать в библио-
теку - файл специальной структуры, содержащий все указанные файлы (все файлы склеены
в один длинный файл, разделяясь специальными заголовками, см. include-файл <ar.h>):
ar r file.a file1.o file2.o file3.o
Будет создана библиотека file.a, содержащая перечисленные .o файлы (имена библиотек в
UNIX имеют суффикс .a - от слова archive, архив). После этого можно использовать
библиотеку:
cc file4.o file5.o file.a -o file
Механизм таков: если в файлах file4.o и file5.o не определена какая-то функция (функ-
ции), то просматривается библиотека, и в список файлов для "склейки" добавляется файл
из библиотеки, содержащий определение этой функции (из библиотеки он не удаляется!).
Тонкость: из библиотеки берутся не ВСЕ файлы, а лишь те, которые содержат определения
недостающих функций|=. Если, в свою очередь, файлы, извлекаемые из библиотеки, будут
содержать неопределенные функции - библиотека (библиотеки) будут просмотрены еще раз
и.т.д. (на самом деле достаточно максимум двух проходов, так как при первом просмотре
библиотеки можно составить ее каталог: где какие функции в ней содержатся и кого
вызывают). Можно указывать и несколько библиотек:
cc file6.c file7.o \
file.a mylib.a /lib/libLIBR1.a -o file
Таким образом, в команде cc можно смешивать имена файлов: исходных текстов на Си .c,
объектных файлов .o и файлов-библиотек .a.
Просмотр библиотек, находящихся в стандартных местах (каталогах /lib и
/usr/lib), можно включить и еще одним способом: указав ключ -l. Если библиотека
называется
/lib/libLIBR1.a или /usr/lib/libLIBR2.a
то подключение делается ключами
-lLIBR1 и -lLIBR2
____________________
|- На самом деле, для "склейки" объектных файлов в выполняемую программу, команда
/bin/cc вызывает программу /bin/ld - link editor, linker, редактор связей, компонов-
щик.
|= Поэтому библиотека может быть очень большой, а к нашей программе "приклеится"
лишь небольшое число файлов из нее. В связи с этим стремятся делать файлы, помещаемые
в библиотеку, как можно меньше: 1 функция; либо "пачка" функций, вызывающих друг
друга.
А. Богатырев, 1992-95 - 78 - Си в UNIX
соответственно.
cc file1.c file2.c file3.o mylib.a -lLIBR1 -o file
Список библиотек и ключей -l должен идти после имен всех исходных .c и объектных .o
файлов.
Библиотека стандартных функций языка Си /lib/libc.a (ключ -lc) подключается
автоматически ("подключить" библиотеку - значит вынудить компилятор просматривать ее
при сборке, если какие-то функции, использованные вами, не были вами определены), то
есть просматривается всегда (именно эта библиотека содержит коды, например, для
printf, strcat, read).
Многие прикладные пакеты функций поставляются именно в виде библиотек. Такие
библиотеки состоят из ряда .o файлов, содержащих объектные коды для различных функций
(т.е. функции в скомпилированном виде). Исходные тексты от большинства библиотек не
поставляются (так как являются коммерческой тайной). Тем не менее, вы можете исполь-
зовать эти функции, так как вам предоставляются разработчиком:
- описание (документация).
- include-файлы, содержащие форматы данных используемые функциями библиотеки
(именно эти файлы включались #include в исходные тексты библ. функций. Теперь
уже вы должны включать их в свою программу).
Таким образом вы знаете, как надо вызывать библиотечные функции и какие структуры
данных вы должны использовать в своей программе для обращения к ним (хотя и не имеете
текстов самих библиотечных функций, т.е. не знаете, как они устроены. Например, вы
часто используете printf(), но задумываетесь ли вы о ее внутреннем устройстве?).
Некоторые библиотечные функции могут быть вообще написаны не на Си, а на ассемблере
или другом языке программирования|-|-. Еще раз обращаю ваше внимание, что библиотека
содержит не исходные тексты функций, а скомпилированные коды (и include-файлы содер-
жат (как правило) не тексты функций, а только описание форматов данных)! Библиотека
может также содержать статические данные, вроде массивов строк-сообщений об ошибках.
Посмотреть список файлов, содержащихся в библиотеке, можно командой
ar tv имяФайлаБиблиотеки
а список имен функций - командой
nm имяФайлаБиблиотеки
Извлечь файл (файлы) из архива (скопировать его в текущий каталог), либо удалить его
из библиотеки можно командами
ar x имяФайлаБиблиотеки имяФайла1 ...
ar d имяФайлаБиблиотеки имяФайла1 ...
где ... означает список имен файлов.
"Лицом" библиотек служат прилагаемые к ним include-файлы. Системные include-
файлы, содержащие общие форматы данных для стандартных библиотечных функций, хранятся
в каталоге /usr/include и подключаются так:
для /usr/include/файл.h надо #include <файл.h>
для /usr/include/sys/файл.h #include <sys/файл.h>
____________________
|-|- Обратите внимание, что библиотечные функции не являются частью ЯЗЫКА Си как та-
кового. То, что в других языках (PL/1, Algol-68, Pascal) является частью языка
(встроено в язык)- в Си вынесено на уровень библиотек. Например, в Си нет оператора
вывода; функция вывода printf - это библиотечная функция (хотя и общепринятая). Та-
ким образом мощь языка Си состоит именно в том, что он позволяет использовать функ-
ции, написанные другими программистами и даже на других языках, т.е. является функци-
онально расширяемым.
А. Богатырев, 1992-95 - 79 - Си в UNIX
(sys - это каталог, где описаны форматы данных, используемых ядром ОС и системными
вызовами). Ваши собственные include-файлы (посмотрите в предыдущий раздел!) ищутся в
текущем каталоге и включаются при помощи
#include "файл.h" /* ./файл.h */
#include "../h/файл.h" /* ../h/файл.h */
#include "/usr/my/файл.h" /* /usr/my/файл.h */
Непременно изучите содержимое стандартных include-файлов в своей системе!
В качестве резюме - схема, поясняющая "превращения" Си-программы из текста на
языке программирования в выполняемый код: все файлы .c могут использовать общие
include-файлы; их подстановку в текст, а также обработку #define произведет препро-
цессор cpp
file1.c file2.c file3.c
| | | "препроцессор"
| cpp | cpp | cpp
| | | "компиляция"
| cc -c | cc -c | cc -c
| | |
file1.o file2.o file3.o
| | |
-----------*-----------
| Неявно добавятся:
ld |<----- /lib/libc.a (библ. станд. функций)
| /lib/crt0.o (стартер)
"связывание" |
"компоновка" |<----- Явно указанные библиотеки:
| -lm /lib/libm.a
V
a.out
1.147. Напоследок - простой, но жизненно важный совет. Если вы пишете программу,
которую вставите в систему для частого использования, поместите в исходный текст этой
программы идентификационную строку наподобие
static char id[] = "This is /usr/abs/mybin/xprogram";
Тогда в случае аварии в файловой системе, если вдруг ваш файл "потеряется" (то есть у
него пропадет имя - например из-за порчи каталога), то он будет найден программой
проверки файловой системы - fsck - и помещен в каталог /lost+found под специальным
кодовым именем, ничего общего не имеющим со старым. Чтобы понять, что это был за
файл и во что его следует переименовать (чтобы восстановить правильное имя), мы при-
меним команду
strings имя_файла
Эта команда покажет все длинные строки из печатных символов, содержащиеся в данном
файле, в частности и нашу строку id[]. Увидев ее, мы сразу поймем, что файл надо
переименовать так:
mv имя_файла /usr/abs/mybin/xprogram
1.148. Где размещать include-файлы и как программа узнает, где же они лежат? Стан-
дартные системные include-файлы размещены в /usr/include и подкаталогах. Если мы
пишем некую свою программу (проект) и используем директивы
#include "имяФайла.h"
А. Богатырев, 1992-95 - 80 - Си в UNIX
то обычно include-файлы имяФайла.h лежат в текущем каталоге (там же, где и файлы с
программой на Си). Однако мы можем помещать ВСЕ наши include-файлы в одно место
(скажем, известное группе программистов, работающих над одним и тем же проектом).
Хорошее место для всех ваших личных include-файлов - каталог (вами созданный)
$HOME/include
где $HOME - ваш домашний каталог. Хорошее место для общих include-файлов - каталог
/usr/local/include
Как сказать компилятору, что #include "" файлы надо брать из определенного места, а
не из текущего каталога? Это делает ключ компилятора
cc -Iимя_каталога ...
Например:
/* Файл x.c */
#include "x.h"
int main(int ac, char *av[]){
....
return 0;
}
И файл x.h находится в каталоге /home/abs/include/x.h (/home/abs - мой домашний ката-
лог). Запуск программы на компиляцию выглядит так:
cc -I/home/abs/include -O x.c -o x
или
cc -I$HOME/include -O x.c -o x
Или, если моя программа x.c находится в /home/abs/progs
cc -I../include -O x.c -o x
Ключ -O задает вызов компилятора с оптимизацией.
Ключ -I оказывает влияние и на #include <> директивы тоже. Для ОС Solaris на
машинах Sun программы для оконной системы X Window System содержат строки вроде
#include <X11/Xlib.h>
#include <X11/Xutil.h>
На Sun эти файлы находятся не в /usr/include/X11, а в /usr/openwin/include/X11. Поэ-
тому запуск на компиляцию оконных программ на Sun выглядит так:
cc -O -I/usr/openwin/include xprogram.c \
-o xprogram -L/usr/openwin/lib -lX11
где -lX11 задает подключение графической оконной библиотеки Xlib.
Если include-файлы находятся во многих каталогах, то можно задать поиск в нес-
кольких каталогах, к примеру:
cc -I/usr/openwin/include -I/usr/local/include -I$HOME/include ...
А. Богатырев, 1992-95 - 81 - Си в UNIX
Массив представляет собой агрегат из нескольких переменных одного и того же
типа. Массив с именем a из LENGTH элементов типа TYPE объявляется так:
TYPE a[LENGTH];
Это соответствует тому, что объявляются переменные типа TYPE со специальными именами
a[0], a[1], ..., a[LENGTH-1]. Каждый элемент массива имеет свой номер - индекс.
Доступ к x-ому элементу массива осуществляется при помощи операции индексации:
int x = ... ; /* целочисленный индекс */
TYPE value = a[x]; /* чтение x-ого элемента */
a[x] = value; /* запись в x-тый элемент */
В качестве индекса может использоваться любое выражение, выдающее значение целого
типа: char, short, int, long. Индексы элементов массива в Си начинаются с 0 (а не с
1), и индекс последнего элемента массива из LENGTH элементов - это LENGTH-1 (а не
LENGTH). Поэтому цикл по всем элементам массива - это
TYPE a[LENGTH]; int indx;
for(indx=0; indx < LENGTH; indx++)
...a[indx]...;
indx < LENGTH равнозначно indx <= LENGTH-1. Выход за границы массива (попытка
чтения/записи несуществующего элемента) может привести к непредсказуемым результатам
и поведению программы. Отметим, что это одна из самых распространенных ошибок.
Статические массивы можно объявлять с инициализацией, перечисляя значения их
элементов в {} через запятую. Если задано меньше элементов, чем длина массива -
остальные элементы считаются нулями:
int a10[10] = { 1, 2, 3, 4 }; /* и 6 нулей */
Если при описании массива с инициализацией не указать его размер, он будет подсчитан
компилятором:
int a3[] = { 1, 2, 3 }; /* как бы a3[3] */
В большинстве современных компьютеров (с фон-Неймановской архитектурой) память
представляет собой массив байт. Когда мы описываем некоторую переменную или массив,
в памяти выделяется непрерывная область для хранения этой переменной. Все байты
памяти компьютера пронумерованы. Номер байта, с которого начинается в памяти наша
переменная, называется адресом этой переменной (адрес может иметь и более сложную
структуру, чем просто целое число - например состоять из номера сегмента памяти и
номера байта в этом сегменте). В Си адрес переменной можно получить с помощью опера-
ции взятия адреса &. Пусть у нас есть переменная var, тогда &var - ее адрес. Адрес
нельзя присваивать целой переменной; для хранения адресов используются указатели
(смотри ниже).
Данное может занимать несколько подряд идущих байт. Размер в байтах участка
памяти, требуемого для хранения значения типа TYPE, можно узнать при помощи операции
sizeof(TYPE), а размер переменной - при помощи sizeof(var). Всегда выполняется
sizeof(char)==1. В некоторых машинах адреса переменных (а также агрегатов данных -
массивов и структур) кратны sizeof(int) или sizeof(double) - это так называемое
"выравнивание (alignment) данных на границу типа int". Это позволяет делать доступ к
данным более быстрым (аппаратура работает эффективнее).
Язык Си предоставляет нам средство для работы с адресами данных - указатели
(pointer)|-. Указатель физически - это адрес некоторой переменной ("указуемой" пере-
менной). Отличие указателей от машинных адресов состоит в том, что указатель может
содержать адреса данных только определенного типа. Указатель ptr, который может ука-
зывать на данные типа TYPE, описывается так:
TYPE var; /* переменная */
TYPE *ptr; /* объявление ук-ля */
ptr = & var;
А. Богатырев, 1992-95 - 82 - Си в UNIX
В данном случае мы занесли в указательную переменную ptr адрес переменной var. Будем
говорить, что указатель ptr указывает на переменную var (или, что ptr установлен на
var). Пусть TYPE равно int, и у нас есть массив и указатели:
int array[LENGTH], value;
int *ptr, *ptr1;
Установим указатель на x-ый элемент массива
ptr = & array[x];
Указателю можно присвоить значение другого указателя на такой же тип. В результате
оба указателя будут указывать на одно и то же место в памяти: ptr1 = ptr;
Мы можем изменять указуемую переменную при помощи операции *
*ptr = 128; /* занести 128 в указуемую перем. */
value = *ptr; /* прочесть указуемую переменную */
В данном случае мы заносим и затем читаем значение переменной array[x], на которую
поставлен указатель, то есть
*ptr означает сейчас array[x]
Таким образом, операция * (значение по адресу) оказывается обратной к операции &
(взятие адреса):
& (*ptr) == ptr и * (&value) == value
Операция * объясняет смысл описания TYPE *ptr; оно означает, что значение выражения
*ptr будет иметь тип TYPE. Название же типа самого указателя - это (TYPE *). В част-
ности, TYPE может сам быть указательным типом - можно объявить указатель на указа-
тель, вроде char **ptrptr;
Имя массива - это константа, представляющая собой указатель на 0-ой элемент мас-
сива. Этот указатель отличается от обычных тем, что его нельзя изменить (установить
на другую переменную), поскольку он сам хранится не в переменной, а является просто
некоторым постоянным адресом.
массив указатель
____________ _____
array: | array[0] | ptr:| * |
| array[1] | |
| array[2] |<--------- сейчас равен &array[2]
| ... |
Следствием такой интерпретации имен массивов является то, что для того чтобы поста-
вить указатель на начало массива, надо писать
ptr = array; или ptr = &array[0];
но не
ptr = &array;
Операция & перед одиноким именем массива не нужна и недопустима!
Такое родство указателей и массивов позволяет нам применять операцию * к имени
массива: value = *array; означает то же самое, что и value = array[0];
Указатели - не целые числа! Хотя физически это и номера байтов, адресная ариф-
метика отличается от обычной. Так, если дан указатель TYPE *ptr; и номер байта
(адрес), на который указывает ptr, равен byteaddr, то
ptr = ptr + n; /* n - целое, может быть и < 0 */
заставит ptr указывать не на байт номер byteaddr + n, а на байт номер
А. Богатырев, 1992-95 - 83 - Си в UNIX
byteaddr + (n * sizeof(TYPE))
то есть прибавление единицы к указателю продвигает адрес не на 1 байт, а на размер
указываемого указателем типа данных! Пусть указатель ptr указывает на x-ый элемент
массива array. Тогда после
TYPE *ptr2 = array + L; /* L - целое */
TYPE *ptr1 = ptr + N; /* N - целое */
ptr += M; /* M - целое */
указатели указывают на
ptr1 == &array[x+N] и ptr == &array[x+M]
ptr2 == &array[L]
Если мы теперь рассмотрим цепочку равенств
*ptr2 = *(array + L) = *(&array[L]) =
array[L]
то получим
ОСНОВНОЕ ПРАВИЛО: пусть ptr - указатель или имя массива. Тогда операции индексации,
взятия значения по адресу, взятия адреса и прибавления целого к указателю связаны
соотношениями:
ptr[x] тождественно *(ptr+x)
&ptr[x] тождественно ptr+x
(тождества верны в обе стороны), в том числе при x==0 и x < 0. Так что, например,
ptr[-1] означает *(ptr-1)
ptr[0] означает *ptr
Указатели можно индексировать подобно массивам. Рассмотрим пример:
/* индекс: 0 1 2 3 4 */
double numbers[5] = { 0.0, 1.0, 2.0, 3.0, 4.0 };
double *dptr = &numbers[2];
double number = dptr[2]; /* равно 4.0 */
numbers: [0] [1] [2] [3] [4]
|
[-2] [-1] [0] [1] [2]
dptr
поскольку
если dptr = &numbers[x] = numbers + x
то dptr[i] = *(dptr + i) =
= *(numbers + x + i) = numbers[x + i]
Указатель на один тип можно преобразовать в указатель на другой тип: такое пре-
образование не вызывает генерации каких-либо машинных команд, но заставляет компиля-
тор изменить параметры адресной арифметики, а также операции выборки данного по ука-
зателю (собственно, разница в указателях на данные разных типов состоит только в раз-
мерах указуемых типов; а также в генерации команд `->' для выборки полей структур,
если указатель - на структурный тип).
Целые (int или long) числа иногда можно преобразовывать в указатели. Этим поль-
зуются при написании драйверов устройств для доступа к регистрам по физическим адре-
сам, например:
А. Богатырев, 1992-95 - 84 - Си в UNIX
unsigned short *KISA5 = (unsigned short *) 0172352;
Здесь возникают два тонких момента:
1. Как уже было сказано, адреса данных часто выравниваются на границу некоторого
типа. Мы же можем задать невыровненное целое значение. Такой адрес будет
некорректен.
2. Структура адреса, поддерживаемая процессором, может не соответствовать формату
целых (или длинных целых) чисел. Так обстоит дело с IBM PC 8086/80286, где адрес
состоит из пары short int чисел, хранящихся в памяти подряд. Однако весь адрес
(если рассматривать эти два числа как одно длинное целое) не является обычным
long-числом, а вычисляется более сложным способом: адресная пара SEGMENT:OFFSET
преобразуется так
unsigned short SEGMENT, OFFSET; /*16 бит: [0..65535]*/
unsigned long ADDRESS = (SEGMENT << 4) + OFFSET;
получается 20-и битный физический адрес ADDRESS
Более того, на машинах с диспетчером памяти, адрес, хранимый в указателе, явля-
ется "виртуальным" (т.е. воображаемым, ненастоящим) и может не совпадать с физи-
ческим адресом, по которому данные хранятся в памяти компьютера. В памяти может
одновременно находиться несколько программ, в каждой из них будет своя система
адресации ("адресное пространство"), отсчитывающая виртуальные адреса с нуля от
начала области памяти, выделенной данной программе. Преобразование виртуальных
адресов в физические выполняется аппаратно.
В Си принято соглашение, что указатель (TYPE *)0 означает "указатель ни на что". Он
является просто признаком, используемым для обозначения несуществующего адреса или
конца цепочки указателей, и имеет специальное обозначение NULL. Обращение (выборка
или запись данных) по этому указателю считается некорректным (кроме случая, когда вы
пишете машинно-зависимую программу и работаете с физическими адресами).
Отметим, что указатель можно направить в неправильное место - на участок памяти,
содержащий данные не того типа, который задан в описании указателя; либо вообще
содержащий неизвестно что:
int i = 2, *iptr = &i;
double x = 12.76;
iptr += 7; /* куда же он указал ?! */
iptr = (int *) &x; i = *iptr;
Само присваивание указателю некорректного значения еще не является ошибкой. Ошибка
подаются в нее в виде аргумента.
Реентерабельные функции независимы от остальной части программы (их можно скопи-
ровать в другой программный проект без изменений), более понятны (поскольку все зат-
рагиваемые ими внешние переменные перечислены как аргументы, не надо выискивать в
теле функции глобальных переменных, передающих значение в/из функции, т.е. эта функ-
ция не имеет побочных влияний), более надежны (хотя бы потому, что компилятор в сос-
тоянии проверить прототип такой функции и предупредить вас, если вы забыли задать
какой-то аргумент; если же аргументы передаются через глобальные переменные - вы
можете забыть проинициализировать какую-то из них). Старайтесь делать функции реен-
терабельными!
А. Богатырев, 1992-95 - 75 - Си в UNIX
Вот еще один пример на эту тему. Не-реентерабельный вариант:
int x, y, result;
int f (){
static int z = 4;
y = x + z; z = y - 1;
return x/2;
}
Вызов: x=13; result = f(); printf("%d\n", y);
А вот реентерабельный эквивалент:
int y, result, zmem = 4;
int f (/*IN*/ int x, /*OUT*/ int *ay, /*INOUT*/ int *az){
*az = (*ay = x + *az) - 1;
return x/2;
}
Вызов: result = f(13, &y, &zmem); printf("%d\n", y);
1.145. То, что формат заголовка функции должен быть известен компилятору до момента
ее использования, побуждает нас помещать определение функции до точки ее вызова. Так,
если main вызывает f, а f вызывает g, то в файле функции расположатся в порядке
g() { }
f() { ... g(); ... }
main(){ ... f(); ... }
Программа обычно разрабатывается "сверху-вниз" - от main к деталям. Си же вынуждает
нас размещать функции в программе в обратном порядке, и в итоге программа читается
снизу-вверх - от деталей к main, и читать ее следует от конца файла к началу!
Так мы вынуждены писать, чтобы удовлетворить Си-компилятор:
#include <stdio.h>
unsigned long g(unsigned char *s){
const int BITS = (sizeof(long) * 8);
unsigned long sum = 0;
for(;*s; s++){
sum ^= *s;
/* cyclic rotate left */
sum = (sum<<1)|(sum>>(BITS-1));
}
return sum;
}
void f(char *s){
printf("%s %lu\n", s, g((unsigned char *)s));
}
int main(int ac, char *av[]){
int i;
for(i=1; i < ac; i++)
f(av[i]);
return 0;
}
А вот как мы разрабатываем программу:
А. Богатырев, 1992-95 - 76 - Си в UNIX
#include <stdio.h>
int main(int ac, char *av[]){
int i;
for(i=1; i < ac; i++)
f(av[i]);
return 0;
}
void f(char *s){
printf("%s %lu\n", s, g((unsigned char *)s));
}
unsigned long g(unsigned char *s){
const int BITS = (sizeof(long) * 8);
unsigned long sum = 0;
for(;*s; s++){
sum ^= *s;
/* cyclic rotate left */
sum = (sum<<1)|(sum>>(BITS-1));
}
return sum;
}
и вот какую ругань производит Си-компилятор в ответ на эту программу:
"0000.c", line 10: identifier redeclared: f
current : function(pointer to char) returning void
previous: function() returning int : "0000.c", line 7
"0000.c", line 13: identifier redeclared: g
current : function(pointer to uchar) returning ulong
previous: function() returning int : "0000.c", line 11
Решением проблемы является - задать прототипы (объявления заголовков) всех функций в
начале файла (или даже вынести их в header-файл).
#include <stdio.h>
int main(int ac, char *av[]);
void f(char *s);
unsigned long g(unsigned char *s);
...
Тогда функции будет можно располагать в тексте в любом порядке.
1.146. Рассмотрим процесс сборки программы из нескольких файлов на языке Си. Пусть
мы имеем файлы file1.c, file2.c, file3.c (один из них должен содержать среди других
функций функцию main). Ключ компилятора -o заставляет создавать выполняемую прог-
рамму с именем, указанным после этого ключа. Если этот ключ не задан - будет создан
выполняемый файл a.out
cc file1.c file2.c file3.c -o file
Мы получили выполняемую программу file. Это эквивалентно 4-м командам:
cc -c file1.c получится file1.o
cc -c file2.c file2.o
cc -c file3.c file3.o
cc file1.o file2.o file3.o -o file
Ключ -c заставляет компилятор превратить файл на языке Си в "объектный" файл
А. Богатырев, 1992-95 - 77 - Си в UNIX
(содержащий машинные команды; не будем вдаваться в подробности). Четвертая команда
"склеивает" объектные файлы в единое целое - выполняемую программу|-. При этом, если
какие-то функции, используемые в нашей программе, не были определены (т.е. спрограм-
мированы нами) ни в одном из наших файлов - будет просмотрена библиотека стандартных
функций. Если же каких-то функций не окажется и там - будет выдано сообщение об
ошибке.
Если у нас уже есть какие-то готовые объектные файлы, мы можем транслировать
только новые Си-файлы:
cc -c file4.c
cc file1.o file2.o file3.o file4.o -o file
или (что то же самое,
но cc сам разберется, что надо делать)
cc file1.o file2.o file3.o file4.c -o file
Существующие у нас объектные файлы с отлаженными функциями удобно собрать в библио-
теку - файл специальной структуры, содержащий все указанные файлы (все файлы склеены
в один длинный файл, разделяясь специальными заголовками, см. include-файл <ar.h>):
ar r file.a file1.o file2.o file3.o
Будет создана библиотека file.a, содержащая перечисленные .o файлы (имена библиотек в
UNIX имеют суффикс .a - от слова archive, архив). После этого можно использовать
библиотеку:
cc file4.o file5.o file.a -o file
Механизм таков: если в файлах file4.o и file5.o не определена какая-то функция (функ-
ции), то просматривается библиотека, и в список файлов для "склейки" добавляется файл
из библиотеки, содержащий определение этой функции (из библиотеки он не удаляется!).
Тонкость: из библиотеки берутся не ВСЕ файлы, а лишь те, которые содержат определения
недостающих функций|=. Если, в свою очередь, файлы, извлекаемые из библиотеки, будут
содержать неопределенные функции - библиотека (библиотеки) будут просмотрены еще раз
и.т.д. (на самом деле достаточно максимум двух проходов, так как при первом просмотре
библиотеки можно составить ее каталог: где какие функции в ней содержатся и кого
вызывают). Можно указывать и несколько библиотек:
cc file6.c file7.o \
file.a mylib.a /lib/libLIBR1.a -o file
Таким образом, в команде cc можно смешивать имена файлов: исходных текстов на Си .c,
объектных файлов .o и файлов-библиотек .a.
Просмотр библиотек, находящихся в стандартных местах (каталогах /lib и
/usr/lib), можно включить и еще одним способом: указав ключ -l. Если библиотека
называется
/lib/libLIBR1.a или /usr/lib/libLIBR2.a
то подключение делается ключами
-lLIBR1 и -lLIBR2
____________________
|- На самом деле, для "склейки" объектных файлов в выполняемую программу, команда
/bin/cc вызывает программу /bin/ld - link editor, linker, редактор связей, компонов-
щик.
|= Поэтому библиотека может быть очень большой, а к нашей программе "приклеится"
лишь небольшое число файлов из нее. В связи с этим стремятся делать файлы, помещаемые
в библиотеку, как можно меньше: 1 функция; либо "пачка" функций, вызывающих друг
друга.
А. Богатырев, 1992-95 - 78 - Си в UNIX
соответственно.
cc file1.c file2.c file3.o mylib.a -lLIBR1 -o file
Список библиотек и ключей -l должен идти после имен всех исходных .c и объектных .o
файлов.
Библиотека стандартных функций языка Си /lib/libc.a (ключ -lc) подключается
автоматически ("подключить" библиотеку - значит вынудить компилятор просматривать ее
при сборке, если какие-то функции, использованные вами, не были вами определены), то
есть просматривается всегда (именно эта библиотека содержит коды, например, для
printf, strcat, read).
Многие прикладные пакеты функций поставляются именно в виде библиотек. Такие
библиотеки состоят из ряда .o файлов, содержащих объектные коды для различных функций
(т.е. функции в скомпилированном виде). Исходные тексты от большинства библиотек не
поставляются (так как являются коммерческой тайной). Тем не менее, вы можете исполь-
зовать эти функции, так как вам предоставляются разработчиком:
- описание (документация).
- include-файлы, содержащие форматы данных используемые функциями библиотеки
(именно эти файлы включались #include в исходные тексты библ. функций. Теперь
уже вы должны включать их в свою программу).
Таким образом вы знаете, как надо вызывать библиотечные функции и какие структуры
данных вы должны использовать в своей программе для обращения к ним (хотя и не имеете
текстов самих библиотечных функций, т.е. не знаете, как они устроены. Например, вы
часто используете printf(), но задумываетесь ли вы о ее внутреннем устройстве?).
Некоторые библиотечные функции могут быть вообще написаны не на Си, а на ассемблере
или другом языке программирования|-|-. Еще раз обращаю ваше внимание, что библиотека
содержит не исходные тексты функций, а скомпилированные коды (и include-файлы содер-
жат (как правило) не тексты функций, а только описание форматов данных)! Библиотека
может также содержать статические данные, вроде массивов строк-сообщений об ошибках.
Посмотреть список файлов, содержащихся в библиотеке, можно командой
ar tv имяФайлаБиблиотеки
а список имен функций - командой
nm имяФайлаБиблиотеки
Извлечь файл (файлы) из архива (скопировать его в текущий каталог), либо удалить его
из библиотеки можно командами
ar x имяФайлаБиблиотеки имяФайла1 ...
ar d имяФайлаБиблиотеки имяФайла1 ...
где ... означает список имен файлов.
"Лицом" библиотек служат прилагаемые к ним include-файлы. Системные include-
файлы, содержащие общие форматы данных для стандартных библиотечных функций, хранятся
в каталоге /usr/include и подключаются так:
для /usr/include/файл.h надо #include <файл.h>
для /usr/include/sys/файл.h #include <sys/файл.h>
____________________
|-|- Обратите внимание, что библиотечные функции не являются частью ЯЗЫКА Си как та-
кового. То, что в других языках (PL/1, Algol-68, Pascal) является частью языка
(встроено в язык)- в Си вынесено на уровень библиотек. Например, в Си нет оператора
вывода; функция вывода printf - это библиотечная функция (хотя и общепринятая). Та-
ким образом мощь языка Си состоит именно в том, что он позволяет использовать функ-
ции, написанные другими программистами и даже на других языках, т.е. является функци-
онально расширяемым.
А. Богатырев, 1992-95 - 79 - Си в UNIX
(sys - это каталог, где описаны форматы данных, используемых ядром ОС и системными
вызовами). Ваши собственные include-файлы (посмотрите в предыдущий раздел!) ищутся в
текущем каталоге и включаются при помощи
#include "файл.h" /* ./файл.h */
#include "../h/файл.h" /* ../h/файл.h */
#include "/usr/my/файл.h" /* /usr/my/файл.h */
Непременно изучите содержимое стандартных include-файлов в своей системе!
В качестве резюме - схема, поясняющая "превращения" Си-программы из текста на
языке программирования в выполняемый код: все файлы .c могут использовать общие
include-файлы; их подстановку в текст, а также обработку #define произведет препро-
цессор cpp
file1.c file2.c file3.c
| | | "препроцессор"
| cpp | cpp | cpp
| | | "компиляция"
| cc -c | cc -c | cc -c
| | |
file1.o file2.o file3.o
| | |
-----------*-----------
| Неявно добавятся:
ld |<----- /lib/libc.a (библ. станд. функций)
| /lib/crt0.o (стартер)
"связывание" |
"компоновка" |<----- Явно указанные библиотеки:
| -lm /lib/libm.a
V
a.out
1.147. Напоследок - простой, но жизненно важный совет. Если вы пишете программу,
которую вставите в систему для частого использования, поместите в исходный текст этой
программы идентификационную строку наподобие
static char id[] = "This is /usr/abs/mybin/xprogram";
Тогда в случае аварии в файловой системе, если вдруг ваш файл "потеряется" (то есть у
него пропадет имя - например из-за порчи каталога), то он будет найден программой
проверки файловой системы - fsck - и помещен в каталог /lost+found под специальным
кодовым именем, ничего общего не имеющим со старым. Чтобы понять, что это был за
файл и во что его следует переименовать (чтобы восстановить правильное имя), мы при-
меним команду
strings имя_файла
Эта команда покажет все длинные строки из печатных символов, содержащиеся в данном
файле, в частности и нашу строку id[]. Увидев ее, мы сразу поймем, что файл надо
переименовать так:
mv имя_файла /usr/abs/mybin/xprogram
1.148. Где размещать include-файлы и как программа узнает, где же они лежат? Стан-
дартные системные include-файлы размещены в /usr/include и подкаталогах. Если мы
пишем некую свою программу (проект) и используем директивы
#include "имяФайла.h"
А. Богатырев, 1992-95 - 80 - Си в UNIX
то обычно include-файлы имяФайла.h лежат в текущем каталоге (там же, где и файлы с
программой на Си). Однако мы можем помещать ВСЕ наши include-файлы в одно место
(скажем, известное группе программистов, работающих над одним и тем же проектом).
Хорошее место для всех ваших личных include-файлов - каталог (вами созданный)
$HOME/include
где $HOME - ваш домашний каталог. Хорошее место для общих include-файлов - каталог
/usr/local/include
Как сказать компилятору, что #include "" файлы надо брать из определенного места, а
не из текущего каталога? Это делает ключ компилятора
cc -Iимя_каталога ...
Например:
/* Файл x.c */
#include "x.h"
int main(int ac, char *av[]){
....
return 0;
}
И файл x.h находится в каталоге /home/abs/include/x.h (/home/abs - мой домашний ката-
лог). Запуск программы на компиляцию выглядит так:
cc -I/home/abs/include -O x.c -o x
или
cc -I$HOME/include -O x.c -o x
Или, если моя программа x.c находится в /home/abs/progs
cc -I../include -O x.c -o x
Ключ -O задает вызов компилятора с оптимизацией.
Ключ -I оказывает влияние и на #include <> директивы тоже. Для ОС Solaris на
машинах Sun программы для оконной системы X Window System содержат строки вроде
#include <X11/Xlib.h>
#include <X11/Xutil.h>
На Sun эти файлы находятся не в /usr/include/X11, а в /usr/openwin/include/X11. Поэ-
тому запуск на компиляцию оконных программ на Sun выглядит так:
cc -O -I/usr/openwin/include xprogram.c \
-o xprogram -L/usr/openwin/lib -lX11
где -lX11 задает подключение графической оконной библиотеки Xlib.
Если include-файлы находятся во многих каталогах, то можно задать поиск в нес-
кольких каталогах, к примеру:
cc -I/usr/openwin/include -I/usr/local/include -I$HOME/include ...
А. Богатырев, 1992-95 - 81 - Си в UNIX
Массив представляет собой агрегат из нескольких переменных одного и того же
типа. Массив с именем a из LENGTH элементов типа TYPE объявляется так:
TYPE a[LENGTH];
Это соответствует тому, что объявляются переменные типа TYPE со специальными именами
a[0], a[1], ..., a[LENGTH-1]. Каждый элемент массива имеет свой номер - индекс.
Доступ к x-ому элементу массива осуществляется при помощи операции индексации:
int x = ... ; /* целочисленный индекс */
TYPE value = a[x]; /* чтение x-ого элемента */
a[x] = value; /* запись в x-тый элемент */
В качестве индекса может использоваться любое выражение, выдающее значение целого
типа: char, short, int, long. Индексы элементов массива в Си начинаются с 0 (а не с
1), и индекс последнего элемента массива из LENGTH элементов - это LENGTH-1 (а не
LENGTH). Поэтому цикл по всем элементам массива - это
TYPE a[LENGTH]; int indx;
for(indx=0; indx < LENGTH; indx++)
...a[indx]...;
indx < LENGTH равнозначно indx <= LENGTH-1. Выход за границы массива (попытка
чтения/записи несуществующего элемента) может привести к непредсказуемым результатам
и поведению программы. Отметим, что это одна из самых распространенных ошибок.
Статические массивы можно объявлять с инициализацией, перечисляя значения их
элементов в {} через запятую. Если задано меньше элементов, чем длина массива -
остальные элементы считаются нулями:
int a10[10] = { 1, 2, 3, 4 }; /* и 6 нулей */
Если при описании массива с инициализацией не указать его размер, он будет подсчитан
компилятором:
int a3[] = { 1, 2, 3 }; /* как бы a3[3] */
В большинстве современных компьютеров (с фон-Неймановской архитектурой) память
представляет собой массив байт. Когда мы описываем некоторую переменную или массив,
в памяти выделяется непрерывная область для хранения этой переменной. Все байты
памяти компьютера пронумерованы. Номер байта, с которого начинается в памяти наша
переменная, называется адресом этой переменной (адрес может иметь и более сложную
структуру, чем просто целое число - например состоять из номера сегмента памяти и
номера байта в этом сегменте). В Си адрес переменной можно получить с помощью опера-
ции взятия адреса &. Пусть у нас есть переменная var, тогда &var - ее адрес. Адрес
нельзя присваивать целой переменной; для хранения адресов используются указатели
(смотри ниже).
Данное может занимать несколько подряд идущих байт. Размер в байтах участка
памяти, требуемого для хранения значения типа TYPE, можно узнать при помощи операции
sizeof(TYPE), а размер переменной - при помощи sizeof(var). Всегда выполняется
sizeof(char)==1. В некоторых машинах адреса переменных (а также агрегатов данных -
массивов и структур) кратны sizeof(int) или sizeof(double) - это так называемое
"выравнивание (alignment) данных на границу типа int". Это позволяет делать доступ к
данным более быстрым (аппаратура работает эффективнее).
Язык Си предоставляет нам средство для работы с адресами данных - указатели
(pointer)|-. Указатель физически - это адрес некоторой переменной ("указуемой" пере-
менной). Отличие указателей от машинных адресов состоит в том, что указатель может
содержать адреса данных только определенного типа. Указатель ptr, который может ука-
зывать на данные типа TYPE, описывается так:
TYPE var; /* переменная */
TYPE *ptr; /* объявление ук-ля */
ptr = & var;
А. Богатырев, 1992-95 - 82 - Си в UNIX
В данном случае мы занесли в указательную переменную ptr адрес переменной var. Будем
говорить, что указатель ptr указывает на переменную var (или, что ptr установлен на
var). Пусть TYPE равно int, и у нас есть массив и указатели:
int array[LENGTH], value;
int *ptr, *ptr1;
Установим указатель на x-ый элемент массива
ptr = & array[x];
Указателю можно присвоить значение другого указателя на такой же тип. В результате
оба указателя будут указывать на одно и то же место в памяти: ptr1 = ptr;
Мы можем изменять указуемую переменную при помощи операции *
*ptr = 128; /* занести 128 в указуемую перем. */
value = *ptr; /* прочесть указуемую переменную */
В данном случае мы заносим и затем читаем значение переменной array[x], на которую
поставлен указатель, то есть
*ptr означает сейчас array[x]
Таким образом, операция * (значение по адресу) оказывается обратной к операции &
(взятие адреса):
& (*ptr) == ptr и * (&value) == value
Операция * объясняет смысл описания TYPE *ptr; оно означает, что значение выражения
*ptr будет иметь тип TYPE. Название же типа самого указателя - это (TYPE *). В част-
ности, TYPE может сам быть указательным типом - можно объявить указатель на указа-
тель, вроде char **ptrptr;
Имя массива - это константа, представляющая собой указатель на 0-ой элемент мас-
сива. Этот указатель отличается от обычных тем, что его нельзя изменить (установить
на другую переменную), поскольку он сам хранится не в переменной, а является просто
некоторым постоянным адресом.
массив указатель
____________ _____
array: | array[0] | ptr:| * |
| array[1] | |
| array[2] |<--------- сейчас равен &array[2]
| ... |
Следствием такой интерпретации имен массивов является то, что для того чтобы поста-
вить указатель на начало массива, надо писать
ptr = array; или ptr = &array[0];
но не
ptr = &array;
Операция & перед одиноким именем массива не нужна и недопустима!
Такое родство указателей и массивов позволяет нам применять операцию * к имени
массива: value = *array; означает то же самое, что и value = array[0];
Указатели - не целые числа! Хотя физически это и номера байтов, адресная ариф-
метика отличается от обычной. Так, если дан указатель TYPE *ptr; и номер байта
(адрес), на который указывает ptr, равен byteaddr, то
ptr = ptr + n; /* n - целое, может быть и < 0 */
заставит ptr указывать не на байт номер byteaddr + n, а на байт номер
А. Богатырев, 1992-95 - 83 - Си в UNIX
byteaddr + (n * sizeof(TYPE))
то есть прибавление единицы к указателю продвигает адрес не на 1 байт, а на размер
указываемого указателем типа данных! Пусть указатель ptr указывает на x-ый элемент
массива array. Тогда после
TYPE *ptr2 = array + L; /* L - целое */
TYPE *ptr1 = ptr + N; /* N - целое */
ptr += M; /* M - целое */
указатели указывают на
ptr1 == &array[x+N] и ptr == &array[x+M]
ptr2 == &array[L]
Если мы теперь рассмотрим цепочку равенств
*ptr2 = *(array + L) = *(&array[L]) =
array[L]
то получим
ОСНОВНОЕ ПРАВИЛО: пусть ptr - указатель или имя массива. Тогда операции индексации,
взятия значения по адресу, взятия адреса и прибавления целого к указателю связаны
соотношениями:
ptr[x] тождественно *(ptr+x)
&ptr[x] тождественно ptr+x
(тождества верны в обе стороны), в том числе при x==0 и x < 0. Так что, например,
ptr[-1] означает *(ptr-1)
ptr[0] означает *ptr
Указатели можно индексировать подобно массивам. Рассмотрим пример:
/* индекс: 0 1 2 3 4 */
double numbers[5] = { 0.0, 1.0, 2.0, 3.0, 4.0 };
double *dptr = &numbers[2];
double number = dptr[2]; /* равно 4.0 */
numbers: [0] [1] [2] [3] [4]
|
[-2] [-1] [0] [1] [2]
dptr
поскольку
если dptr = &numbers[x] = numbers + x
то dptr[i] = *(dptr + i) =
= *(numbers + x + i) = numbers[x + i]
Указатель на один тип можно преобразовать в указатель на другой тип: такое пре-
образование не вызывает генерации каких-либо машинных команд, но заставляет компиля-
тор изменить параметры адресной арифметики, а также операции выборки данного по ука-
зателю (собственно, разница в указателях на данные разных типов состоит только в раз-
мерах указуемых типов; а также в генерации команд `->' для выборки полей структур,
если указатель - на структурный тип).
Целые (int или long) числа иногда можно преобразовывать в указатели. Этим поль-
зуются при написании драйверов устройств для доступа к регистрам по физическим адре-
сам, например:
А. Богатырев, 1992-95 - 84 - Си в UNIX
unsigned short *KISA5 = (unsigned short *) 0172352;
Здесь возникают два тонких момента:
1. Как уже было сказано, адреса данных часто выравниваются на границу некоторого
типа. Мы же можем задать невыровненное целое значение. Такой адрес будет
некорректен.
2. Структура адреса, поддерживаемая процессором, может не соответствовать формату
целых (или длинных целых) чисел. Так обстоит дело с IBM PC 8086/80286, где адрес
состоит из пары short int чисел, хранящихся в памяти подряд. Однако весь адрес
(если рассматривать эти два числа как одно длинное целое) не является обычным
long-числом, а вычисляется более сложным способом: адресная пара SEGMENT:OFFSET
преобразуется так
unsigned short SEGMENT, OFFSET; /*16 бит: [0..65535]*/
unsigned long ADDRESS = (SEGMENT << 4) + OFFSET;
получается 20-и битный физический адрес ADDRESS
Более того, на машинах с диспетчером памяти, адрес, хранимый в указателе, явля-
ется "виртуальным" (т.е. воображаемым, ненастоящим) и может не совпадать с физи-
ческим адресом, по которому данные хранятся в памяти компьютера. В памяти может
одновременно находиться несколько программ, в каждой из них будет своя система
адресации ("адресное пространство"), отсчитывающая виртуальные адреса с нуля от
начала области памяти, выделенной данной программе. Преобразование виртуальных
адресов в физические выполняется аппаратно.
В Си принято соглашение, что указатель (TYPE *)0 означает "указатель ни на что". Он
является просто признаком, используемым для обозначения несуществующего адреса или
конца цепочки указателей, и имеет специальное обозначение NULL. Обращение (выборка
или запись данных) по этому указателю считается некорректным (кроме случая, когда вы
пишете машинно-зависимую программу и работаете с физическими адресами).
Отметим, что указатель можно направить в неправильное место - на участок памяти,
содержащий данные не того типа, который задан в описании указателя; либо вообще
содержащий неизвестно что:
int i = 2, *iptr = &i;
double x = 12.76;
iptr += 7; /* куда же он указал ?! */
iptr = (int *) &x; i = *iptr;
Само присваивание указателю некорректного значения еще не является ошибкой. Ошибка