кового. То, что в других языках (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
2. Массивы, строки, указатели.
Массив представляет собой агрегат из нескольких переменных одного и того же
типа. Массив с именем 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-ый элемент