C. Глава 4. Стандартная библиотека libc

Feb 24, 2018 13:15


"Сама по себе библиотека не является частью языка, однако, заложенный в ней набор функций, а также определений типов и макросов составляет системную среду, поддерживающую стандарт Си."

(Б. Керниган, Д. Ритчи "Язык программирования C")

1. Библиотека как набор функций
Если заглянуть в наш каталог c/bin, то там настоящее нагромождение файлов. Еще немного и недолго запутаться или потерять что-нибудь важное. Пришло время навести порядок. Мы можем собрать все объектные модули отдельных функций в один архивный файл, откуда компоновщик будет извлекать их по мере необходимости.

Если посмотреть на размер наших последних программ, то он окажется относительно большим (последняя версия strings весит 3272 байта, а это страшно много по нашим масштабам). Это происходит оттого, что мы до сих пор связываем все подряд объектные модули, и в нашей программе получается, так называемый, мертвый код. Использование правильно созданной библиотеки исключит его.

На самом низком уровне компьютер оперирует с простейшими типами данных и занимается пересылкой байтов и машинных слов, из одного места в другое. Даже арифметических операций он выполняет не так уж много. Дать на таком уровне описание сколько-нибудь сложной задачи, в виде, понятном человеку, совершенно невозможно. Поэтому и были придуманы такие абстракции, как процедуры и функции. Берем какой-нибудь алгоритм, оборачиваем его в форму вызываемой процедуры или функции и пользуемся в свое удовольствие.

Причем, такая операция может быть многоуровневой. С математической точки зрения, получается композиция функций, часто неявная, пример - использование strchr в функции strtok в самом конце прошлой главы. (Явная была бы использованием возвращаемого значения одной функции в качестве аргумента другой.) Это делает код более компактным, но создает проблему зависимостей, которая может оказаться большой головной болью в случае ошибок.

Все это лишь говорит о том, что создание библиотеки, тем более стандартной библиотеки, является чем-то вроде производства станков в машиностроении. Чтобы изготовить деталь широкого применения с заданной точностью, необходимы станки, работающие с точностью на порядки выше. Так что, вся наша книжка до сих пор является довольно амбициозным проектом, каким бы упрощенным он не объявлялся и какой бы протоптанной дорожкой мы не шли. Мораль состоит в том, что при разработке библиотек должны быть приняты все посильные меры к устранению любых ошибок.

Вторая особенность библиотек заключается в том, что - за исключением стандартных и run time - они посвящаются какой-то определенной области, это специализированный инструмент, заточенный под свою работу. Даже стандартная библиотека, строго говоря, это тоже специализированная вещь: ее назначение - обеспечить какие-то базовые операции в языке, подобно тому, как ребенок учится пользоваться окружающими предметами, ходить и узнавать близких. Это та самая "системная среда" о которой говорят Керниган и Ритчи.

Наконец, есть еще и третье свойство: библиотека может быть статической или динамической. Случай статической библиотеки мы как раз имеем в ближайшей перспективе. Код, в этом случае, намертво заделывается в исполняемый файл программы и загружается в память целиком, при запуске приложения. Его преимущество в мгновенной готовности к выполнению. Недостатком здесь является неотвратимый рост размера программы при ее усложнении, а самое главное, избыточное дублирование и неоправданный перерасход памяти.

Именно поэтому в библиотеки статической компоновки обычно помещается самое основное и наиболее важное, то, без чего никак нельзя обойтись. В отличии от статических библиотек, динамические подгружаются по мере необходимости. Динамические библиотеки есть не только в Windows, где они известны как файлы dll. В UNIX-системах всех типов динамические библиотеки имеют расширение .so (shared object) Использование динамических библиотек позволяет сильно уменьшить объем кода, торчащего в памяти, за счет его разделения программами, он может использоваться совместно. Как минимум, если даже только одна программа использует библиотеку, то она может загружать ее при необходимости, какой может вовсе не случиться, если пользователь не обращается к некоторым из функций приложения, обеспечиваемым из этой библиотеки.

Любые библиотеки, кроме функций могут также содержать данные или константы. Они могут быть просто функцией, заданной в табличном виде. Такой прием используют, когда функция вычисляется долго, или не существует быстро сходящегося ряда, по которому вычисляются ее значения или наконец, вообще никакого, кроме таблицы, способа задания такой функции не существует.

2. Собираем свою библиотеку
Для начала необходимо устроить ревизию всех исходников с функциями, которые мы написали за все время. Нужно удалить метку exit из crt0.s, удалить это имя также из списка глобальных имен модуля (см. листинг 2-4). Он должен принять свой первоначальный вид, как на Рис. 0-4. Вместо этого напишем модуль exit.s по образцу read или write:

.file "exit.c"
.text
.globl exit
exit:
movq $60, %rax
syscall
ret

Лст. 4-1. Функция exit.

Обязательно испытаем функцию exit:

//exittst.c

main()
{
exit(7);
}

. . . . . . . .

$ m exittst
$ exittst
$ echo $?
7
$

Лст. 4-2. Испытание нормальной функции exit.

Также перепишем функции strcmp и strncmp, указывая тип первых двух аргументов как unsigned char:



Рис. 4-1. Версия strcmp для библиотеки.

Мы уточняем тип аргументов прямо в коде функции, при ее определении. Если это не поможет, то придется использовать заголовочные файлы, которые до сих пор не использовались. Возможно, эта книжка, на тему языка C, попадет в книгу рекордов Гиннеса по неиспользованию заголовочных файлов до четвертой главы.

После проверки исходников будет желательно перестроить каталоги. Файлы в каталоге bin лучше не удалять, вместо этого можно закатать их в архив:

$ tar -czf ../bin.tar.gz $(ls)
$ du -b ../bin.tar.gz
37987 ../bin.tar.gz

Лст. 4-3. Способ архивирования всех файлов из каталога.

Как видим, архив хорошо жмется. После этого архив можно переместить в другой каталог, например, doc, а исходные файлы из bin можно перемещать в отдельный специальный каталог. Но не все! Нам ведь еще понадобятся команды m, c и еще пара файлов:

#!/bin/bash

as $1.s -o $1.o

Лст. 4-4. Файл a - команда вызова ассемблера.

Еще один полезный файл:

PATH=$PWD:$PATH

Лст. 4-5. Файл p для настройки переменой PATH.

Последний файл не надо делать исполняемым! Используется он так: сначала открывается терминал в рабочем каталоге, затем в командной строке набираем точку, обязательно пробел и имя файла: $ . p

Точка - это команда оболочки прочитать файл в командную строку. Аналог директивы include из ассемблера или препроцессора C. В оболочке используется для загрузки пользовательских функций и переменных (как в нашем случае). Только помните о пробеле между точкой и именем файла. Точка непосредственно перед именем файла считается частью имени файла и в UNIX означает просто скрытый файл. Вы можете сделать p скрытым и использовать как: $ . .p Но это не выглядит хорошей идеей.

Создадим теперь два каталога: c/src и c/lib. В первом будут исходники для библиотеки, а в lib будет сами библиотеки: crt0.o и libc.a В src переместим все наши исходники с функциями. (Понятно, что тестовые программы там не нужны. Для них у нас есть созданный архив.) В общем, все должно выглядеть так, за исключением каталога doc, который у вас может быть своим или его вовсе может не быть:



Рис. 4-2. Новая структура подкаталогов c.

В каталоге src находится файл makefile. Это очень важная штука, сейчас речь пойдет как раз об этом файле.

#
# makefile для libc.a
#
# зависит от исполняемых файлов a и c
# во внешнем каталоге

ASM = ../bin/a
CC = ../bin/c

# список модулей в библиотеке

OBJS = write.o read.o exit.o getc.o putc.o \
getchar.o putchar.o atoi.o itoa.o atol.o \
ltoa.o puts.o fgets.o strlen.o strcpy.o \
strcat.o strset.o strchr.o strstr.o \
strncat.o strncpy.o strcmp.o strncmp.o \
strtok.o

# сборка библиотеки

libc.a: $(OBJS)
ar -ru libc.a $(OBJS)

# модули из ассемблера

write.o: write.s
$(ASM) write
read.o: read.s
$(ASM) read
exit.o: exit.s
$(ASM) exit

# модули из C

getc.o: getc.c
$(CC) getc
putc.o: putc.c
$(CC) putc
getchar.o: getchar.c
$(CC) getchar
putchar.o: putchar.c
$(CC) putchar
atoi.o: atoi.c
$(CC) atoi
itoa.o: itoa.c
$(CC) itoa
atol.o: atol.c
$(CC) atol
ltoa.o: ltoa.c
$(CC) ltoa
puts.o: puts.c
$(CC) puts
fgets.o: fgets.c
$(CC) fgets
strlen.o: strlen.c
$(CC) strlen
strcpy.o: strcpy.c
$(CC) strcpy
strcat.o: strcat.c
$(CC) strcat
strset.o: strset.c
$(CC) strset
strchr.o: strchr.c
$(CC) strchr
strstr.o: strstr.c
$(CC) strstr
strncat.o: strncat.c
$(CC) strncat
strncpy.o: strncpy.c
$(CC) strncpy
strcmp.o: strcmp.c
$(CC) strcmp
strncmp.o: strncmp.c
$(CC) strncmp
strtok.o: strtok.c
$(CC) strtok

# модуль run time

crt:
$(ASM) crt0

# полная очистка

clean:
rm *.o

# замена старой версии библиотеки

replace:
mv crt0.o libc.a ../lib

# -------------- конец файла ----------------

Лст. 4-6. makefile для сборки библиотеки.

Формат makefile предусматривает строки с правилами и командами. Строка с правилами определяет цель и зависимость, а строка (или строки) с командами ниже выполняется над зависимым объектом, чтобы получить цель. Файл просматривается сверху вниз и выполняется в обратном порядке. Есть особенность: любая строка с командой обязательно должна начинаться с табуляции, - это отличительный признак командных строк.

Мейкфайл выполняется утилитой make. Это "менеджер проектов" для командной строки. Надо сказать, что это действительно, очень эффективный манагер, без всякой натяжки. Многие IDE автоматически создают такой файл, когда юзер выберет шаблон проекта или добавляет в него новые модули исходного кода или файлы ресурсов (в Windows все C/C++ компиляторы тоже используют консольную утилиту make, ибо вся IDE - не что иное, как костыль для домохозяек).

Утилита make была создана для облегчения компиляции сложных проектов. Голова программиста в те далекие дни, в некотором смысле, работала быстрее компилятора. При любых изменениях приходилось перерабатывать весь проект, а компьютеры давних лет работали довольно неторопливо.

Программистам приходилось идти курить или заваривать себе чай или кофе, пока компилятор трудился. Тогда и придумали утилиту, которая просто отслеживает время изменения файлов, и обрабатывает только те файлы, которые были изменены со времени последнего запуска. Это заметно повысило производительность труда программистов.

Сейчас, а в нашем случае, тем более, можно позволить себе роскошь перекомпилировать даже сложные проекты из множества файлов, - мощь домашнего ПК или ноутбука не сравнить с машинами тех лет, когда была создана make (1976). Но зачем делать лишнюю работу?

В приведенном примере (листинг 4-6) использованы явные правила. Расписаны все действия по полочкам. Существуют также неявные правила, которые позволяют заметно сократить размер makefile и время на его написание. Чтобы использовать мейкфайл с неявными правилами, придется немного переделать скрипт для вызова компилятора. Скопируем c в c1 и перепишем его так:

#!/bin/bash

# специальная версия команды компиляции
# для использования в make-файлах

name=${1%.*} # отделяет имя от расширения

cc -S $1 -fno-builtin -o - | sed '
/\.type/d
/\.LF/d
/\.cfi/d
/\.ident/d
/\.-main/d
/\.section/d' > $name.s

as $name.s -o $name.o

rm $name.s

Лст. 4-7. Команда компиляции, требующая имя файла с расширением.

Расширение имени файла нам очень понадобится в новой версии makefile:

#
# makefile для libc.a версия 2
#
# зависит от исполняемого файла c1
# во внешнем каталоге

CC = ../bin/c1

# список модулей в библиотеке

OBJS = write.o read.o exit.o getc.o putc.o \
getchar.o putchar.o atoi.o itoa.o atol.o \
ltoa.o puts.o fgets.o strlen.o strcpy.o \
strcat.o strset.o strchr.o strstr.o \
strncat.o strncpy.o strcmp.o strncmp.o \
strtok.o

# сборка библиотеки

libc.a: $(OBJS)
ar -ru libc.a $(OBJS)
%.o : %.s
as $< -o $@
%.o : %.c
$(CC) $<
crt:
as crt0.s -o crt0.o
clean:
rm *.o
replace:
cp crt0.o libc.a ../lib

# -------------- конец файла ----------------

Лст. 4-8. Makefile с неявными правилами.

Такой файл выглядит намного заковыристей, но проще обслуживается. Он заслуживает более подробного описания, чем предыдущий. В переменной CC хранится команда вызова компилятора (C Compiler). Это путь к shell-скрипту c1. Вызов ассемблера теперь будет делаться непосредственно, а это уже другой формат аргументов чем в прежней "обертке" команде a. (Которая становится ненужной.)

Список модулей хранится в переменной OBJS в виде длинной строки, образованной из нескольких строк и обратной косой черты (\), которая "склеивает" строки. В разных системах могут быть разные ограничения на длину командных строк, но командная строка, вообще-то, не резиновая. В случае строк слишком большой длины можно использовать внешний файл, из которого утилита ar будет читать данные.

Конечная цель - файл libc.a, собираемый из модулей в макрокоманде $(OBJS). Этот макрос просто расширяется в командной строке. libc.a зависит от модулей: в случае изменения любого из них будут проверяться остальные правила. В следующей строке - не забывайте про табуляцию в самом начале строки! - помещается команда вызова архиватора. Ключи -ru означают, что модули заменяются в библиотеке (r), и заменяются только те, которые нужно обновить (u).

Дальше идет неявное правило с шаблоном зависимостей. Файлы .o зависящие от файлов .s - это те объектные модули, которые зависят от одноименных ассемблерных исходников, например write, read или exit. Как только изменяется какой-нибудь исходник, вызывается команда с соответствующими аргументами. Кстати, и об аргументах. Встроенные макро: $< является файлом-зависимостью, а $@ - файлом-целью. При вызове команды происходит подстановка. Тут без расширения никак. Поэтому и понадобилось изменять скрипт для компиляции, делать дополнительный к c. Именно ради следующего правила все и затевалось.

Предпоследняя зависимость в библиотечном makefile - вызов компилятора для измененных исходников на языке C. Мы просто передаем скрипту файл C вместе с расширением, что учитывает скрипт c1 из листинга 4-7. И наконец, для библиотеки времени выполнения crt0 мы имеем отдельное явное правило, которое выполняется особой командой (как и все остальное ниже).

Теперь все собрано и готово к запуску.

3. Компиляция библиотеки
Каких либо особенностей при компиляции библиотек, отличающих этот процесс от сборки обычных приложений, не существует. Библиотека или исполняемый файл - это всего лишь способ обращения с файлом со стороны операционной системы. Но поскольку эта работа выполняется с использованием нового инструмента, утилиты make, то стоит описать все подробно. Это не займет много времени, так как большая часть уже описана.

Нужно убедиться, что переменная PATH в работающем экземпляре терминала подготовлена (теперь с помощью файла p) и перейти в каталог src. В этом каталоге у нас находится вся коллекция исходных файлов, ассемблерных и файлов C. Здесь же находится makefile. Теперь достаточно выполнить команду make:

$ make
as write.s -o write.o
as read.s -o read.o
as exit.s -o exit.o
../bin/c1 getc.c
../bin/c1 putc.c
../bin/c1 getchar.c
../bin/c1 putchar.c
../bin/c1 atoi.c
../bin/c1 itoa.c
../bin/c1 atol.c
../bin/c1 ltoa.c
../bin/c1 puts.c
../bin/c1 fgets.c
../bin/c1 strlen.c
../bin/c1 strcpy.c
../bin/c1 strcat.c
../bin/c1 strset.c
../bin/c1 strchr.c
../bin/c1 strstr.c
strstr.c: In function ‘strstr’:
strstr.c:6:7: warning: assignment makes pointer from
integer without a cast [enabled by default]
f = strchr( s, *n );
^
../bin/c1 strncat.c
../bin/c1 strncpy.c
../bin/c1 strcmp.c
../bin/c1 strncmp.c
../bin/c1 strtok.c
ar -ru libc.a write.o read.o exit.o getc.o putc.o
getchar.o putchar.o atoi.o itoa.o atol.o ltoa.o
puts.o fgets.o strlen.o strcpy.o strcat.o strset.o
strchr.o strstr.o strncat.o strncpy.o strcmp.o
strncmp.o strtok.o
ar: creating libc.a
$

Лст. 4-9. Выполнение makefile.

Все происходит автоматически, make сама ищет файл makefile в текущем каталоге и выполняет его.

В одном из исходных файлов компилятор нашел к чему придраться. Откроем и исправим ошибку:

//strchr.c

char *strchr( char *s, int c )
{
char *p = s;
while( *p != c && *p != '\0' ) p++;
return *p == c ? p : 0;
return (char*)0;
}

Лст. 4-10. Исправление ошибки в strchr.

Ошибка находится не в том файле - strstr, который обрабатывал компилятор, а в зависимом от него - strchr. Это простой, но поучительный пример, связанный с зависимостями. Не нравится возвращаемое целое? Тогда вернем нулевой указатель, с помощью явного преобразования типа (выделено цветом). Сохраним файл и снова запустим make:

$ make
../bin/c1 strchr.c
ar -ru libc.a write.o read.o exit.o getc.o putc.o
getchar.o putchar.o atoi.o itoa.o atol.o ltoa.o
puts.o fgets.o strlen.o strcpy.o strcat.o strset.o
strchr.o strstr.o strncat.o strncpy.o strcmp.o
strncmp.o strtok.o
$

Лст. 4-11. Второй вызов make.

Здесь видно, что make действительно, выполнила только ту часть работы, которая требуется. Из этого листинга неочевидно, собирался ли библиотечный файл заново из всех модулей или был заменен только модуль strchr.o Но поверим ключам командной строки.

Файл crt0.o стоит отдельно. Он не входит в стандартную библиотеку и совершенно незачем его туда помещать. Он получается отдельно:

$ make crt
as crt0.s -o crt0.o
$

Лст. 4-12. Получение модуля crt0.o.

Раз его сборка прошла без замечаний, то теперь мы можем экспортировать библиотеки в каталог, специально для них предназначенный:

$ make replace
cp crt0.o libc.a ../lib
$

Лст. 4-13. Замена версии библиотек.

Команда называется "заменить", что точнее выражает суть в случае обновления библиотек, независимо от того, пополняются они новыми модулями или в них делается замена уже существующих. Остальное все практически очевидно. makefile можно дополнить командой бэкапа, которая сохраняет все исходники на всякий случай, но это можно сделать и любым внешним образом.

Теперь можно приступать к проверке работоспособности библиотек.

4. Работа с готовой библиотекой
Вернемся к нашей последней программе strings.c (листинг 3-25). Возьмем только часть ее кода:

//strings.c

char str[] = " ,ab cde Вася ef,dh xy z, xyz,ab z";
char *ptr;

main()
{
ptr = strtok( str, " ," );

while( ptr )
{
puts( ptr );
ptr = strtok( 0, " ," );
}
}

Лст. 4-14. Версия strings с использованием библиотек.

Работать будем по-прежнему в каталоге bin, поскольку наши проекты еще слишком простые, для такой роскоши, как отдельный каталог. Библиотека, конечно, составляет исключение. Это непростой проект, да к тому же еще и активно развивающийся, будем надеяться.

Нам потребуются небольшие изменения в файле m, который мы используем для простых проектов. Теперь нет необходимости держать в нем множество записей об объектных модулях. Хватит одной ссылки на файл библиотеки:

#!/bin/bash

cc -S $1.c -fno-builtin -o - | sed '
/\.type/d
/\.LF/d
/\.cfi/d
/\.ident/d
/\.-main/d
/\.section/d' > $1.s

as $1.s -o $1.o
ld -s $1.o ../lib/crt0.o ../lib/libc.a -o $1

Лст. 4-15. Новая версия скрипта m.

$ m strings
strings.c: In function ‘main’:
strings.c:8:9: warning: assignment makes pointer from
integer without a cast [enabled by default]
ptr = strtok( str, " ," );
^
strings.c:13:13: warning: assignment makes pointer from
integer without a cast [enabled by default]
ptr = strtok( 0, " ," );
^
$ strings
ab
cde
Вася
ef
dh
xy
z
xyz
ab
z
$ du -b strings
1320 strings
$

Лст. 4-16. Все почти хорошо.

На этот раз мы видим программу значительно меньшего размера, всего 1320 байтов против 3272 в старой версии. Это результат удаления, точнее, невключения в исполняемый файл мертвого кода. Другими словами, компоновщик взял из библиотеки ровно то, что нужно для работы программы.

Исправить небольшую ошибку в файле strtok теперь не составит никакого труда: она полностью аналогична предыдущей в strchr. Но дело не в этом, легко сделать это технически: перейти в каталог src, открыть файл strstr.c, поправить его, выполнить make, make replace и затем снова вернуться в рабочий каталог и выполнить m strings. То же самое можно проделать с любым файлом в котором обнаружится аналогичная ошибка.

Но компилятор упорно выводит предупреждающее сообщение об использовании целого вместо указателя без преобразования типа в функции strtok! В чем тут дело? Что за дела, мать его?

А это значит, что пришло время использовать:

5. Заголовочные файлы
Перепишем программу в виде:

//strings.c

char *strtok( char*, char* ); // объявление функции

char str[] = " ,ab cde Вася ef,dh xy z, xyz,ab z";
char *ptr;

main()
{
ptr = strtok( str, " ," );

while( ptr )
{
puts( ptr );
ptr = strtok( 0, " ," );
}
}

Лст. 4-17. Использование объявления перед вызовом функции.

Теперь все выглядит как раньше:

$ m strings
$ strings
ab
cde
Вася
ef
dh
xy
z
xyz
ab
z
$

Лст. 4-18. Предупреждений нет.

Можно отменить все ненужные преобразования типов при возвращаемых нулях в функциях и переделать библиотеку. Там все было правильно. Все дело в понятиях объявления и определения функции. До сих пор, написание кода функции выше основной программы было одновременно и объявлением и определением функции. Но на этот раз код берется из библиотеки. Однако, компилятор у нас не имеет никакой информации о типе, возвращаемом из функции. По умолчанию он думает, что это целый тип. Ведь функция была просто вызвана, но не объявлена. И если мы поместим перед вызовом функции ее объявление, то компилятор будет знать, как с ней обращаться.

Так будет обстоять дело с любой из вызываемых функций, код которых находится либо во внешнем файле, либо даже в том же самом файле, но после функции, в которой он вызывается. Можно, конечно, выписывать объявления для всех используемых файлов, раз уж компилятору нужно их видеть. Но есть более простой способ - можно косвенно включить в файл объявления всех функций, в том числе, и не используемых. Компилятор сам разберется, что использовать. Тем не менее, объявления функций, констант и макросов разделены на определенные группы, и каждая группа включает свой набор функций. Эти группы содержатся в заголовочных файлах, headers, которые имеют расширения .h Заголовочные файлы хранятся еще в одном каталоге, include (включаемые). Компилятор имеет специальную опцию, которая позволяет указать ему путь к заголовочным файлам. То же самое, кстати, предусмотрено для библиотечных файлов у компоновщика.

Все функции, которые были написаны у нас до сих пор, можно разнести по трем группам, которые иногда называют "библиотеками", хотя стандартная библиотека, конечно, одна. Вот как это выглядит:

stdio.h: getc putc getchar putchar puts fgets...
stdlib.h: atoi atol itoa ltoa exit...
string.h: strlen strcpy strcat strset strchr strncpy strncat strcmp strncmp strstr strtok...

Это, разумеется, далеко не все, что есть в упомянутых заголовочных файлах. И самих заголовков немало, гораздо больше трех. Здесь всего лишь то, что охватывает написанное нами к этому моменту. Многоточия показывают, что нам есть что еще добавлять в библиотеку.

Создадим каталог c/include. В этот каталог мы поместим три файла:

//stdio.h - функции для ввода и вывода -

int getc( int f );

int putc( int c, int f );

#define getchar() getc( 0 )
#define putchar(c) putc( c, 1 )

int puts( char *s );

char *fgets( char *s, int count, int fd );

// ----------------- eof ---------------

Лст. 4-19. Заголовочный файл stdio.h

Здесь мы заменили пару функций макросами. Функции getchar и putchar слишком просты, в них не производится никакой содержательной работы. Макрофункции сэкономят несколько машинных команд, а поскольку getchar и putchar применяются достаточно часто, то это должно положительно повлиять и на размер кода, и на скорость выполнения.

//stdlib.h - стандартные функции -

int atoi( char *s );

long atol( char *s );

char *itoa( int v, int b );

char *ltoa( long v, int b );

int exit( int r );

//------------- eof --------------

Лст. 4-20. Заголовочный файл stdlib.h

//string.h - функции для строк -

int strlen( char *s );

char *strcpy( char *d, char *s );

char *strcat( char *d, char *s );

char *strset( char *s, int c );

char *strchr( char *s, int c );

char *strset( char *s, int c );

char *strncpy( char *d, char *s, int n );

char *strncat( char *d, char *s, int n );

int strcmp( unsigned char *s0,
unsigned char *s1 );

int strncmp( unsigned char *s0,
unsigned char *s1, int n );

char *strtok( char *s, char *d );

//------------ eof -------------

Лст. 4-21. Заголовочный файл string.h

В хорошо документированных компиляторах заголовочные файлы снабжаются подробными комментариями, поясняющими интерфейс функций: что означают аргументы и возвращаемые значения. Вы можете сами сделать это.

Поскольку мы изменили функции getchar и putchar, то прежняя версия библиотеки стала несовместимой с этим заголовком. Для исправления мы должны удалить функции getchar и putchar в исходном коде всех функций, зависимых от getchar и putchar и использовать вместо них getc и putc. Также целесообразно удалить и сами модули getchar.o и putchar.o из библиотеки. И конечно, исходники putchar.c и getchar.c

Но сначала посмотрим, где используются getchar и putchar:



Рис. 4-3. В какие файлы входят имена putchar и getchar.

Нам везет! Используется только putchar в файле puts.c. (В функции fgets мы использовали getc.)

Теперь выполним все задачи по обновлению библиотеки. Сначала сохраним текущую версию всех исходных файлов:

$ tar -czf src1.tar.gz $(ls ../src/*.{c,s})
tar: Removing leading `../' from member names
$ mv src1.tar.gz ../doc

Лст. 4-22. Резервная копия исходных файлов.

Удалим из библиотеки модули getchar и putchar, объектные модули и исходные файлы с соответствующими именами:

$ cd ../src
$ ar -dv libc.a getchar.o putchar.o
d - getchar.o
d - putchar.o
$ ar -t libc.a
. . . . . .

Лст. 4-23. Удаление модулей из библиотеки.

Удалим и остальное, чтобы его вновь не затащило в библиотеку. Для этого необходимо прежде всего исправить переменную $(OBJS) в makefile, убрав оттуда имена getchar.o и putchar.o. Также неплохо удалить и файлы getchar* и putchar* с диска (имеются в виду объектные и исходные модули). Хотя эта книжка уже не потянет на рекорд в книге Гиннеса, по избежанию использования заголовочных файлов, но точно потянет по числу упоминаний getchar и putchar в одной главе.

Исправим файл puts.c в двух местах:

//puts.c

int puts( char *s )
{
int e;
while( *s )
{
e = putchar( *s++, 1 );
if( e == -1 ) break;
}
putсhar( '\n', 1 );
return e;
}

Лст. 4-24. Исправления в функции puts.

Уберем красное и добавим желтое. То есть, заменим текст на вызов функции putc для потока stdout. Сохраним и закроем файл puts.c Затем выполним make и make replace. Библиотека обновлена.

Внесем изменения в файл m:

#!/bin/bash

inc="-I../include"
lib="../lib/crt0.o ../lib/libc.a"

cc -S $1.c $inc -fno-builtin -o - | sed '
/\.type/d
/\.LF/d
/\.cfi/d
/\.ident/d
/\.-main/d
/\.section/d' > $1.s

as $1.s -o $1.o
ld -s $1.o $lib -o $1

rm $1.s $1.o

Лст. 4-25. Пути к файлам заголовков и библиотек.

Пути к файлам заголовков и библиотек теперь указаны с помощью переменных оболочки inc и lib.

В файл strings.c остается добавить как минимум одну директиву include:

#include

char str[] = " ,ab cde Вася ef,dh xy z, xyz,ab z";
char *ptr;

main()
{
ptr = strtok( str, " ," );

while( ptr )
{
puts( ptr );
ptr = strtok( 0, " ," );
}
}

Лст. 4-26. Включение заголовочного файла.

При компиляции эта программа не приведет ни к каким предупреждениям. Хотя мы не включили сюда файл заголовка stdio.h, в котором объявлена функция puts. puts не возвращает никакого значения, то есть, возвращаемое значение ничему не присваивается. К тому же, тип возвращаемого значения - целое число. Это то, что функция должна вернуть по умолчанию.

И кстати, размер программы, после обновления библиотеки, стал еще меньше: 1256 байт.

Все, что находится под хешем (диез, решетка) # в начале строки, передается препроцессору, это его директивы. Он подставляет вместо определений констант их значения, расширяет макрокоманды, и включает в текст все файлы под директивой #include. Угловые скобки, в которых находится имя включаемого файла показывает компилятору, что надо искать файл в системных каталогах компилятора. А чтобы компилятор не навязывал нам собственные, "стандартные" ("штатные", "родные", "заводские"), называйте как угодно, заголовки, мы используем путь к нашему собственному "системному" каталогу при вызове компилятора (строчка 3 из листинга 4-25).

Что делает препроцессор, можно легко увидеть при помощи утилиты cpp (это он и есть собственной персоной):

$ cpp -nostdinc -I../include strings.c -o strings.i
$

Лст. Лст. 4-27. Вызов препроцессора в командной строке.

Ключ -nostdinc довольно прозрачно говорит сам о себе: не включать стандартные заголовки. Ключ -I это путь к нашему каталогу c/include.

Посмотрим, что в файле strings.i:

# 1 "strings.c"
# 1 ""
# 1 ""
# 1 "strings.c"

# 1 "../include/string.h" 1

int strlen( char *s );

char *strcpy( char *d, char *s );

char *strcat( char *d, char *s );

char *strset( char *s, int c );

char *strchr( char *s, int c );

char *strset( char *s, int c );

char *strncpy( char *d, char *s, int n );

char *strncat( char *d, char *s, int n );

int strcmp( unsigned char *s0,
unsigned char *s1 );

int strncmp( unsigned char *s0,
unsigned char *s1, int n );

char *strtok( char *s, char *d );
# 4 "strings.c" 2

char str[] = " ,ab cde Вася ef,dh xy z, xyz,ab z";
char *ptr;

main()
{
ptr = strtok( str, " ," );

while( ptr )
{
puts( ptr );
ptr = strtok( 0, " ," );
}
}

Лст. 4-28. Образ программы, передаваемый компилятору.

Таким образом, все становится видным компилятору. Заголовочные файлы могут находиться не только в системном каталоге, они могут быть, и часто бывают, в рабочем каталоге. В этом случае, имя файла и путь к нему, если он есть, заключаются в двойные кавычки, как обычная строка.

Ну, вот наконец, закончилась эта глава. Не ожидал, что она окажется такой большой. В следующей главе займемся аргументами главной функции, функции main. Эта тема важная и представляет большой интерес.


Дальше

препроцессор, заголовки, #include, c, библиотеки кода, использование makefile

Previous post Next post
Up