|
Программирование в Linux. Учебное пособие С. В. Шапошникова, Лаборатория юного линуксоида, май 2012 1 Пояснительная записка
Решение задач Задание 1. Напишите программу, которая запрашивает у пользователя имя (адрес) текстового файла, далее открывает его и считает в нем количество символов и строк. 2. Напишите программу, которая записывает в файл данные, полученные из другого файла и так или иначе измененные перед записью. Каждая строка данных, полученная из файла, должна помещаться в структуру. Урок 18. Аргументы программы (или функции main()) Бывает, что данные в программу передаются из командной строки при ее вызове. Такие данные называются аргументами командной строки. Выглядит это так, например: ./a.out test.txt ls -lt /home/peter/ Здесь вызываются программы a.out (из текущего каталога) и ls (из одного каталога, указанного в переменной окружения PATH). Первая программа из командной строки получает одно слово — test.txt, вторая — два: -lt и /home/peter/. Если программа написана на языке C , то при ее запуске управление сразу передается в функцию main() , следовательно, именно она получает аргументы командной строки, которые присваиваются ее переменным-параметрам. До этого мы определяли функцию main() так, как-будто она не принимает никакие параметры и ничего не возвращает. На самом деле в языке C любая функция по-умолчанию (если не определено ничего иного) возвращает целое число. В этом можно убедиться. Если записать код таким образом: main() { printf("Hi\n"); return 0; } , то никакого предупреждения или ошибки при компиляции не возникнет. То же самое будет, если записать int main() . Это доказывает, что функция по-умолчанию возвращает целое число, а не ничто ( void ). Хотя то, что возвращает функция всегда можно "переопределить", например, void main() или float main() При вызове программы из командной строки в нее всегда передается пара данных: 1. целое число, обозначающее количество слов (элементов, разделенных пробелами) в командной строке при вызове, 2. указатель на массив строк, где каждая строка — это отдельное слово из командной строки. 71
Следует иметь в виду, что само имя программы также считается. Например, если вызов выглядит так: ./a.out 12 theme 2 , то первый аргумент программы имеет значение 4, а массив строк определяется как {"./a.out", "12", "theme", "2"}. Обратите внимание на терминологию, есть всего два аргумента программы (число и массив), но сколько угодно аргументов командной строки. Аргументы командной строки "преобразуются" в аргументы программы (в аргументы функции main() ). Эти данные (число и указатель) передаются в программу даже тогда, когда она просто вызывается по имени без передачи в нее чего-либо: ./a.out. В таком случае первый аргумент имеет значение 1, а второй указывает на массив, состоящий всего из одной строки {"./a.out"}. То, что в программу передаются данные, вовсе не означает, что функция main() должна их принимать. Если функция main() определена без параметров, то получить доступ к аргументам командной строки невозможно. Хотя ничего вам не мешает их передавать. Ошибки не возникнет. Чтобы получить доступ к переданным в программу данным, их необходимо присвоить переменным. Поскольку аргументы сразу передаются в main() , то ее заголовок должен выглядеть таким образом: main (int n, char *arr[]) В первой переменной (n) содержится количество слов, а во второй — указатель на массив строк. Часто второй параметр записывают в виде **arr . Однако это то же самое. Вспомним, что сам массив строк, содержит в качестве своих элементов указатели на строки. А в функцию мы передаем указатель на первый элемент массива. Получается, что передаем указатель на указатель, т.е. **arr Задание Напишите такую программу: #include main(int argc, char **argv) { int i; printf("%d\n", argc); for (i=0; i < argc; i++) puts(argv[i]); } Она выводит количество слов в командной строке при ее вызове и каждое слово с новой строки. Вызовите ее без аргументов командной строки и с аргументами. В программе мы использовали переменные-параметры argc и argv. Принято использовать именно такие имена, но на самом деле они могут быть любыми. Лучше придерживаться этого стандарта, чтобы ваши программы были более понятны не только вам, но и другим программистам. Практическое значение передачи данных в программу Если у вас есть опыт работы в командной строке GNU/Linux, вы знаете, что у большинства команд есть ключи и аргументы. Например, при просмотре содержимого каталогов, копировании, перемещении в качестве аргументов указываются объекты файловой системы, над которыми выполняется команда. Особенности ее выполнения определяются с помощью ключей. Например, в команде 72
cp -r ../les_1 ../les_101 cp — это имя команды, -r — ключ, а ../les_1 и ../les_101 — аргументы команды. Вообще чаще всего в программы при их запуске передаются адреса файлов и "модификаторы" (это ключи) процесса выполнения программы. Напишем программу, которая открывает указанные пользователем в командной строке файлы на запись или добавление и записывает (добавляет) туда одну и туже информацию, которую пользователь вводит с клавиатуры в процессе выполнения программы: #include #include main (int argc, char **argv) { int i, ch; FILE *f[5]; if (argc < 3 || argc > 7) { puts("Неверное количество параметров"); return 1; } if (strcmp(argv[1], "-w") != 0 && strcmp(argv[1], "-a") != 0) { puts("Первый параметр может быть либо -w, либо -a"); return 2; } for (i=0; i < argc-2; i++){ f[i] = fopen(argv[i+2], argv[1]+1); if (f[i] == NULL) { printf("Файл %s нельзя открыть\n", argv[i+2]); return 3; } } while ((ch = getchar()) != EOF) for (i=0; i < argc-2; i++) putc(ch,f[i]); for (i=0; i < argc-2; i++) fclose(f[i]); return 0; } Пояснения к коду: 1. Создается массив из пяти файловых указателей. Следовательно можно одновременно открыть не более пяти файлов. Файловый указатель первого файла будет хранится в элементе массива f[0], второго — в f[1] и т.д. 2. Проверяется количество аргументов командной строки. Их должно быть не меньше трех, т.к. первый - это имя программы, второй — режим открытия файла, третий — первый или единственный файл, в который будет производится запись. Поскольку программа позволяет открыть только пять файлов, то общее число аргументов командной строки не может быть больше семи. Поэтому если количество аргументов меньше 3 или больше 7, то программа завершается, т.к. оператор return приводит к выходу из функции, даже если после него есть еще код. Возвращаемое из функции значение неравное 0, может быть интерпретировано родительским процессом, как сообщение о том, что программа завершилась с ошибкой. 3. Проверяется корректность второго аргумента командной строки. Если он не равен ни 73
"-w", ни "-a", то условное выражение во втором if возвращает 1 (true). Функция strcmp() позволяет сравнивать строки и возвращает 0 в случае их равенства. 4. В цикле for открываются файлы по указанным адресам, которые начинаются с третьего элемента массива argv. Именно поэтому к i прибавляется 2, чтобы получать элементы массива argv, начиная с третьего. Выражение argc-2 указывает на количество переданных имен файлов; т.к. в argc хранится общее число аргументов командной строки, первые два из которых не являются именами файлов. 5. Выражение argv[1]+1 позволяет "вырезать" из строки "-w" (или "-a") подстроку "w" (или "a"), т.к. argv[1] по сути указатель на первый элемент строки. Прибавляя к указателю единицу, мы смещаем его к следующему элементу массива. 6. Если файл отрыть не удается, то функция fopen() возвращает NULL. В таком случае программа завершается. 7. Каждый символ, введенный пользователем с клавиатуры, записывается во все открытые файлы. 8. В конце файлы закрываются. Задание Придумайте свой пример использования аргументов командной строки в программе. Реализуйте его. Урок 19. Препроцессор языка С В компилятор языка программирования C входит так называем препроцессор, который осуществляет подготовку программы к компиляции. Среди прочего он, например, включает содержимое одних файлов в другие, заменяет в тексте исходного кода имена констант на их значения, удаляет символы конца строки (которые нужны только программисту, чтобы код можно было легко читать, но не нужны компилятору). Что-то препроцессор делает по- умолчанию, а какие-то его действия программируются с помощью специальных директив в исходном коде. Директивы препроцессора начинаются со знака # и заканчиваются переходом на новую строку. В отличие от законченного выражения на языке C , в конце директив не надо ставить точку с запятой. Ниже рассматриваются наиболее распространенные директивы препроцессора и некоторые его свойства, но это далеко не все, что может делать препроцессор. Директива #include С этой директивой мы уже не раз встречались, подключая заголовочные файлы стандартной библиотеки языка, содержащие объявления (прототипы) функций. Когда препроцессор встречает такую директиву, то понимает, что после нее идет имя файла, и включает все содержимое указанного файла в исходный код программы. Поэтому объем кода вашей программы после обработки ее препроцессором может сильно увеличиться. Если имя файла после директивы #include заключено в угловые скобки (например, ), то поиск заголовочного файла производится в стандартном (специально оговоренном системой) каталоге. Однако в тексте программы может встречаться и такая запись: #include "ext.h" В таком случае заголовочный файл в первую очередь будет искаться в текущем каталоге. Таким образом, программист сам может определять заголовочные файлы для своих проектов. Кроме того, можно указывать адрес заголовочного файла: 74
#include "/home/iam/project10/const.h" Директива #defineСимволические константыС директивой препроцессора #define мы также уже знакомы. С ее помощью объявляются и определяются так называемые символические константы. Например: #define N 100 #define HELLO "Hello. Answer the next questions, please." Когда перед компиляцией исходный код будет обработан препроцессором, то все символьные константы (в примере это N и HELLO) в тексте исходного кода на языке C будут заменены на соответствующие им числовые или строковые константы. Символические константы можно определять в любом месте исходного кода. Однако чтобы переопределить их (изменить значение), необходимо отменить предыдущее определение. Иначе возникнет если не ошибка, то предупреждение. Для удаления символической константы используют директиву #undef : #include #define HELLO "Hello. Answer the next questions, please.\n" main () { printf(HELLO); #undef HELLO #define HELLO "Good day. Tell us about you.\n" printf(HELLO); } Если в этом примере убрать строку #undef HELLO , то при компиляции в GNU/Linux появляется предупреждение: "HELLO" переопределён. Символические константы принято писать заглавными буквами. Это только соглашение для удобства чтения кода. Макросы как усложненные символьные константы С помощью директивы #define можно заменять символьными константами не только числовые и строковые константы, но почти любую часть кода: #include #define N 100 #define PN printf("\n") #define SUM for(i=0; iint i, sum = 0; SUM; printf("%d", sum); PN; } Здесь в теле функции main() PN заменяется препроцессором на printf("\n") , а SUM на цикл for . Такие макроопределения (макросы) в первую очередь удобны, когда в программе часто встречается один и тот же код, но выносить его в отдельную функцию нет смысла. 75
В примере выше PN и SUM являются макросами без аргументов. Однако препроцессор языка программирования C позволяет определять макросы с аргументами: #include #define DIF(a,b) (a) > (b) ? (a)-(b) : (b)-(a) main () { int x = 10, y = 30; printf("%d\n", DIF(67,90)); printf("%d\n", DIF(876-x,90+y)); } Вызов макроса DIV(67,90) в тексте программы приводит к тому, что при обработке программы препроцессором туда подставляется такое выражение (67) > (90) ? (67)- (90) : (90)-(67) . В этом выражении вычисляется разница между двумя числами с помощью условного выражения (см. урок 3). В данном случае скобки не нужны. Однако при таком разворачивании (876-x) > (90+y) ? (876-x)-(90+y) : (90+y)-(876-x) скобки подчеркивают порядок операций. Если бы вместо сложения и вычитания фигурировали операции умножения или деления, то наличие скобок было бы принципиальным. Обратите внимание, что в имени макроса не должно быть пробелов: DIF(a,b) . Первый пробел после идентификатора означает конец символической константы и начало выражения для подстановки. Задание Напишите программу, содержащую пару макросов: один вычисляет сумму элементов массива, другой выводит элементы массива на экран. Напишите программу, содержащую макросы с аргументами, вычисляющие площади различных геометрических фигур (например, квадрата, прямоугольника, окружности). Директивы условной компиляции Так называемая условная компиляция позволяет компилировать или не компилировать части кода в зависимости от наличия символьных констант или их значения. Условное выражение для препроцессора выглядит в сокращенном варианте так: #if … … #endif То, что находится между #if и #endif выполняется, если выражение при #if возвращает истину. Находится там могут как директивы препроцессора так и исходный код на языке C Следует знать, что условное включение может быть расширено за счет веток #else и #elif Рассмотрим несколько примеров. Если в программе константа N не равна 0, то цикл for выполнится, и массив arr заполнится нулями. Если N определена и равна 0, или не определена вообще, то цикл выполняться не будет: #include #define N 10 main() { int i, arr[100]; 76
#if N for(i=0; iarr[i] = 0; printf("%d ", arr[i]); } #endif printf("\n"); } Если нужно выполнить какой-то код в зависимости от наличия символьной константы, а не ее значения, то директива #if будет выглядеть так: #if defined(N) Или сокращенно (что тоже самое): #ifdef N Когда нет уверенности, была ли определена ранее символьная константа, то можно использовать такой код: #if !defined(N) #define N 100 #endif Таким образом мы определим константу N, если она не была определена ранее. Такие проверки могут встречаться в многофайловых проектах. Выражение препроцессора #if ! defined(N) может быть сокращено так: #ifndef N Следует знать, что условную компиляцию иногда используют при отладке программного кода, а также с ее помощью компилируют программы под конкретные операционные системы. Помните, что препроцессор обрабатывает программу до компиляции. В двоичном коде уже отсутствуют какие-либо условные выражения для препроцессора. Поэтому в логическом выражении "препроцессорного if" не должно содержаться переменных, значение которых определяется в момент выполнения программы. Задание Придумайте и напишите программу, которая может быть скомпилирована по-разному в зависимости от того, определена или нет в ней какая-либо символьная константа. Константы, определенные препроцессором Препроцессор самостоятельно определяет пять констант. От обычных (определенных программистом) они отличаются наличием пары символов подчеркивания в начале и конце их имени. • __DATE__ - дата компиляции; • __FILE__ - имя компилируемого файла; • __LINE__ - номер текущей строки исходного текста программы; • __STDC__ - равна 1, если компилятор работает по стандарту ANSI для языка C; • __TIME__ - время компиляции. Если эти константы встречаются в тексте программы, то заменяются на соответствующие 77
строки или числа. Т.к. это происходит до компиляции, то, например, мы видим дату компиляции, а не дату запуска программы на выполнение. Программа ниже выводит значение предопределенных препроцессором имен на экран: #include #define NL printf("\n") main () { printf(__DATE__); NL; printf("%d",__LINE__); NL; printf(__FILE__); NL; printf(__TIME__); NL; printf("%d",__STDC__); NL; } Результат: Mar 22 2012 7 macronames.c 10:07:04 1 Урок 20. Многофайловые программы Объектный код Запуск gcc позволяет обработать файл с исходным кодом препроцессором и далее скомпилировать его. Однако при этом сам инструмент gcc не компилирует файл исходного кода в конечный исполняемый файл. Он компилирует его в объектный файл, после чего вызывает так называемый линковщик, или компоновщик. Но зачем надо сначала получать объектный файл, а потом из него уже исполняемый? Для программ, состоящих из одного файла, такой необходимости нет. Хотя при желании здесь также можно отказаться от компоновки, если выполнить команду gcc с ключом -c: gcc -c hello.c В результате получится файл с расширением *.o. Чтобы получить из объектного файла исполняемый, надо использовать ключ -o: gcc -o hello hello.o Задание Получите из любого ранее написанного файла с исходным кодом на языке C объектный файл, а затем из него исполняемый. Для программ, состоящих из нескольких файлов исходного кода, получение объектных файлов является необходимым. Именно из них потом компонуется единственный исполняемый файл. Компиляция программы, состоящей из нескольких файлов исходного кода Рассмотрим пример. Пусть в одном файле определена пара функций, а в другом, содержащем функцию main() , осуществляется их вызов. Файл superprint.c: #include void l2r (char **c, int n) { 78
int i, j; for(i=0; ifor (j=0; jprintf("\t"); printf ("%s\n",*c); } } void r2l (char **c, int n) { int j; for(; n>0; n--, c++) { for (j=1; jprintf("\t"); printf ("%s\n",*c); } } Файл main.c: #include #define N 5 main () { char strs[N][10]; char *p[N]; int i; for(i=0; iscanf("%s", strs[i]); p[i] = &strs[i][0]; } l2r(p, N); r2l(p, N); } В теле функции main() заполняется массив, состоящий из строк, а также массив указателей на эти строки. Далее в функции l2r() и r2l() передаются ссылки на первый элемент массива указателей и значение символической константы N. Эти функции осуществляют вывод элементов массива строк с отступами. Чтобы получить исполняемый файл этой программы, надо сначала получить объектные файлы из исходных: gcc -c superprint.c gcc -c main.c Тоже самое можно сделать за один вызов gcc: gcc -c superprint.c main.c Или даже вот так, если в каталоге находятся только файлы текущего проекта: gcc -c *.c В любом случае в каталоге появятся два объектных файла: superprint.o и main.o. Далее их можно скомпилировать в один исполняемый файл так: gcc -o myprint main.o superprint.o или так: gcc -o myprint *.o Обратите внимание на то, что в данном случае обязательно требуется указывать имя исполняемого файла. Такой вариант недопустим: 79
gcc -o main.o superprint.o Если теперь запустить файл myprint, то программа будет ожидать ввода пяти слов, после чего выведет их на экран два раза по-разному (с помощью функций l2r() и r2l() ): Задумаемся, каким образом в представленной выше программе функция main() "узнает" о существовании функций l2r() и r2l() . Ведь в исходном коде файла main.c нигде не указано, что мы подключаем файл superprint.c, содержащий эти функции. Действительно, если попытаться получить из main.c отдельный исполняемый файл, т.е. скомпилировать программу без superprint.c: gcc main.c , то ничего не получиться. Компилятор сообщит об ошибке вызова неопределенных идентификаторов. Получить из файла superprint.c отдельный исполняемый файл вообще невозможно, т.к. там отсутствует функция main() . А вот получить из этих файлов отдельные объектные файлы можно. Представим, что одни объектные файлы как бы "выставляют наружу" имена определенных в них функций и глобальных переменных, а другие - вызовы этих имен из тел других функций. Дальше объектные файлы "ожидают", что имена будут связаны с их вызовами. Связывание происходит при компиляции исполняемого файла из объектных. Задание Придумайте и напишите свой пример программы, состоящей из двух-трех файлов исходного кода. Создание заголовочных файлов Продолжим разбирать приведенную выше программу. Что будет, если в функции main() осуществить неправильный вызов функций l2r() и r2l() ? Например, указать неверное количество параметров. В таком случае создание объектных файлов пройдет без ошибок, и скорее всего удастся получить исполняемый файл; но вот работать программа будет неправильно. Такое возможно потому, что ничего не контролирует соответствие вызовов прототипам (объявлениям) функций. Задание В вашей программе в функции main() вызовите функцию из другого файла с неверным количеством параметров. Попробуйте скомпилировать программу и запустить исполняемый файл. Куда правильней сообщать о неверном вызове функций уже при получении объектного файла. Поэтому хотя можно обойтись и без этого, но очень желательно сообщать функции main() прототипы функций, которые из нее вызываются. Это можно сделать, прописав объявления функций в файле main.c: … void l2r (char **c, int n); void r2l (char **c, int n); main () { 80
… Теперь, если мы передадим неправильные параметры, ошибка возникнет уже на этапе получения объектных файлов. А теперь представим, что программа у нас несколько больше и содержит десяток файлов исходного кода. Файл aa.c требует функций из файла bb.c, dd.c, ee.c. В свою очередь dd.c вызывает функции из ee.c и ff.c, а эти два последних файла активно пользуются неким файлом stars.c и одной из функций в bb.c. Программист замучится сверять, что чего вызывает откуда и куда, где и какие объявления надо прописывать. Поэтому все прототипы (объявления) функций проекта, а также совместно используемые символические константы и макросы выносят в отдельный файл, который подключают к каждому файлу исходного кода. Такие файлы называются заголовочными; с ними мы уже не раз встречались. В отличие от заголовочных файлов стандартной библиотеки, заголовочные файлы, которые относятся только к вашему проекту, при подключении к файлу исходного кода заключаются в кавычки, а не скобки. Об этом упоминалось в предыдущем уроке. Итак, более грамотно будет не добавлять объявления функций в файл main.c, а создать заголовочный файл, например, myprint.h и поместить туда прототипы функций l2r() и r2l() . А в файле main.c следует прописать директиву препроцессора: #include "myprint.h" В принципе смысла подключать myprint.h к файлу superprint.c в данном случае нет, т.к. последний не использует никаких сторонних функций, кроме стандартной библиотеки. Но если планируется расширять программу и есть вероятность, что в файле superprint.c будут вызываться сторонние для него функции, то будет надежней сразу подключить заголовочный файл. Задание Создайте заголовочный файл для вашей программы и подключите его к файлам исходного кода. Обратим внимание еще на один момент. Стоит ли в описанном в этом уроке примере выносить константу N в заголовочный файл? Здесь нельзя дать однозначный ответ. Если ее туда вынести, то она станет доступна в обоих файлах, и поэтому можно изменить прототипы функций так, чтобы они принимали только один параметр (указатель), а значение N будет известно функциям их заголовочного файла. Однако стоит ли так делать? В функции r2l() второй параметр изменяется в процессе ее выполнения, что делать с константой будет невозможно. Придется переписывать тело функции. Кроме того, вдруг в последствии нам захочется использовать файл superprint.c в другом проекте, где будут свои порядки, и константы N в заголовочном файле не найдется. В таком случае лучше N не выносить в заголовочный файл. Хотя имейте в виду, в каких-то проектах символическая константа может использоваться так часто и во множестве функций, что ее будет лучше поместить в заголовочный файл. Особенности использования глобальных переменныхПомните, если можно отказаться от использования глобальных переменных, то лучше это сделать. Желательно стремиться к тому, чтобы любой файл проекта, скажем так, "не лез к соседу за данными, а сосед не разбрасывал эти данные в виде глобальных переменных". Обмен данными между функциями должен осуществлять за счет передачи данных в качестве параметров и возврата значений с помощью оператора return . (Массивов это не касается.) Однако в языке программирования C есть проблема на этот счет. С помощью return можно вернуть только одно значение. Но могут быть случаи, когда функция должна изменить несколько переменных (здесь не имеются ввиду элементы массива). В таком случае, 81 возможно, без глобальных переменных не обойтись. Так как же их использовать, если вдруг? • Если в файле aa.c объявлена переменная за пределами любой функции (например, так: int count ), то она является глобальной для всех файлов проекта. Чтобы получить значение этой переменной в файле aa.c достаточно просто указать ее имя (если в функции нет локальной переменной с тем же именем). Чтобы получить значение из других файлов, надо указать, что имеется в виду глобальная переменная, а не локальная. Делается это с помощью ключевого слова extern (например, extern count ). • Бывают ситуации, когда в одном файле для нескольких содержащихся в нем функций нужна глобальная переменная. Но эта переменная не должна быть доступна функциям, содержащимся в других файлах. В таком случае глобальная переменная объявляется с ключевым словом static (например, static int count ). Тем самым мы как бы скрываем глобальную переменную. Задание Напишите простые наглядные примеры, использования глобальных функций: 1. Объявите глобальную переменную в одном файле, а получите ее значение в другом файле (выведите на экран). 2. Объявите в одном файле статическую глобальную переменную. Выведите ее значение на экран из функции данного файла. Попытайтесь сделать это из функции другого файла. 3. Создайте две глобальные переменные в одном файле. В другом файле напишите функцию, которая меняет их значение. Урок 21. Библиотеки Библиотеки позволяют использовать разработанный ранее программный код в различных программах. Таким образом, программист может не разрабатывать часть кода для своей программы, а воспользоваться тем, что входит в состав библиотек. Обычно код библиотек отличается качеством, позволяет писать более ясный код, понятный большинству программистов. В языке программирования C код библиотек представляет собой функции, размещенные в файлах, которые скомпилированы в объектные файлы, а те, в свою очередь, объединены в библиотеки. В одной библиотеке объединяются функции, решающие определенный тип задач. Например, существует библиотека математических функций. У каждой библиотеки должен быть свой заголовочный файл, в котором должны быть описаны прототипы (объявления) всех функций, содержащихся в этой библиотеке. С помощью заголовочных файлов вы "сообщаете" вашему программному коду, какие библиотечные функции есть и как их использовать. При компиляции программы библиотеки подключаются линковщиком, который вызывается gcc. Если программе требуются только стандартные библиотеки, то дополнительных параметров линковщику передавать не надо (есть исключения). Он "знает", где стандартные библиотеки находятся, и подключит их автоматически. Во всех остальных случаях при компиляции программы требуется указать имя библиотеки и ее местоположение. Библиотеки бывают двух видов — статические и динамические. Код первых при компиляции полностью входит в состав исполняемого файла, что делает программу легко переносимой. Код динамических библиотек не входит в исполняемый файл, последний содержит лишь ссылку на библиотеку. Если динамическая библиотека будет удалена или перемещена в другое место, то программа работать не будет. С другой стороны, использование динамических библиотек позволяет сократить размер исполняемого файла. 82
Также если в памяти находится две программы, использующие одну и туже динамическую библиотеку, то последняя будет загружена в память лишь единожды. Далее будет описан пример, в котором создается библиотека, после чего используется при создании программы. Пример создания библиотекиДопустим, мы хотим создать код, который в дальнейшем планируем использовать в нескольких проектах. Следовательно, нам требуется создать библиотеку. Исходный код для библиотеки было решено разместить в двух файлах исходного кода. Также на данный момент у нас есть план первого проекта, использующего эту библиотеку. Сам проект также будет включать два файла. В итоге, когда все будет сделано, схема каталогов и файлов будет выглядеть так: Пусть каталоги library и project находятся в одном общем каталоге, например, домашнем каталоге пользователя. Каталог library содержит каталог source с файлами исходных кодов библиотеки. Также в library будут находиться заголовочный файл (содержащий описания функций библиотеки), статическая (libmy1.a) и динамическая (libmy2.so) библиотеки. Каталог project будет содержать файлы исходных кодов проекта и заголовочный файл с описанием функций проекта. Также после компиляции с подключением библиотеки здесь будет располагаться исполняемый файл проекта. В операционных системах GNU/Linux имена файлов библиотек должны иметь префикс "lib", статические библиотеки - расширение *.a, динамические - *.so. Для компиляции проекта достаточно иметь только одну библиотеку: статическую или динамическую. В образовательных целях мы получим обе и сначала скомпилируем проект со статической библиотекой, потом — с динамической. Статическая и динамическая "разновидности" одной библиотеки по-идее должны называться одинаково (различаются только расширения). Поскольку у нас обе библиотеки будут находиться в одном каталоге, то чтобы быть уверенными, что при компиляции проекта мы используем ту, которую хотим, их названия различны (libmy1 и libmy2). Исходный код библиотекиФайл figure.c: void rect (char sign, int width, int height) { int i, j; 83 for (i=0; i < width; i++) putchar(sign); putchar('\n'); for (i=0; i < height-2; i++) { for (j=0; j < width; j++) { if (j==0 || j==width-1) putchar(sign); else putchar(' '); } putchar('\n'); } for (i=0; i < width; i++) putchar(sign); putchar('\n'); } void diagonals (char sign, int width) { int i, j; for (i=0; i < width; i++) { for (j=0; j < width; j++) { if (i == j || i+j == width-1) putchar(sign); else putchar(' '); } putchar('\n'); } } В файле figure.c содержатся две функции — rect() и diagonals() . Первая принимает в качестве аргументов символ и два числа и "рисует" на экране с помощью указанного символа прямоугольник заданной ширины и высоты. Вторая функция выводит на экране две диагонали квадрата ("рисует" крестик). Файл text.c: void text (char *ch) { while (*ch++ != '\0') putchar('*'); putchar('\n'); } В файле text.c определена единственная функция, принимающая указатель на символ строки. Функция выводит на экране звездочки в количестве, соответствующем длине указанной строки. Файл mylib.h: void rect (char sign, int width, int height); void diagonals (char sign, int width); void text (char *ch); Заголовочный файл можно создать в каталоге source, но мы лучше сохраним его там, где будут библиотеки. В данном случае это на уровень выше (каталог library). Тем самым как бы подчеркивается, что файлы исходных кодов после создания из них библиотеки вообще не нужны пользователям библиотек, они нужны лишь разработчику библиотеки. А вот заголовочный файл библиотеки требуется для ее правильного использования. Создание статической библиотеки Статическую библиотеку создать проще, поэтому начнем с нее. Она создается из обычных объектных файлов путем их архивации с помощью утилиты ar. Все действия, которые описаны ниже выполняются в каталоге library (т.е. туда надо перейти командой cd). Просмотр содержимого каталога выполняется с помощью команды ls или ls -l. 84
Получаем объектные файлы: gcc -c ./source/*.c В итоге в каталоге library должно наблюдаться следующее: figures.o mylib.h source text.o Далее используем утилиту ar для создания статической библиотеки: ar r libmy1.a *.o Параметр r позволяет вставить файлы в архив, если архива нет, то он создается. Далее указывается имя архива, после чего перечисляются файлы, из которых архив создается. Объектные файлы нам не нужны, поэтому их можно удалить: rm *.o В итоге содержимое каталога library должно выглядеть так: libmy1.a mylib.h source , где libmy1.a — это статическая библиотека. Создание динамической библиотеки Объектные файлы для динамической библиотеки компилируются особым образом. Они должны содержать так называемый позиционно-независимый код (position independent code). Наличие такого кода позволяет библиотеке подключаться к программе, когда последняя загружается в память. Это связано с тем, что библиотека и программа не являются единой программой, а значит как угодно могут располагаться в памяти относительно друг друга. Компиляция объектных файлов для динамической библиотеки должна выполняться с опцией -fPIC компилятора gcc: gcc -c -fPIC source/*.c В отличие от статической библиотеки динамическую создают при помощи gcc указав опцию -shared: gcc -shared -o libmy2.so *.o Использованные объектные файлы можно удалить: rm *.o В итоге содержимое каталога library: libmy1.a libmy2.so mylib.h source Использование библиотеки в программе Исходный код программы Теперь в каталоге project (который у нас находится на одном уровне файловой иерархии с library) создадим файлы проекта, который будет использовать созданную библиотеку. Поскольку сама программа будет состоять не из одного файла, то придется здесь также создать заголовочный файл. Файл data.c: #include #include "../library/mylib.h" void data (void) { char strs[3][30]; char *prompts[3] = {"Ваше имя: ", "Местонахождение: ", "Пунк прибытия: "}; int i; for (i=0; i<3; i++) { 85
printf("%s", prompts[i]); gets(strs[i]); } diagonals('', 7); for (i=0; i<3; i++) { printf("%s", prompts[i]); text(strs[i]); } } Функция data() запрашивает у пользователя данные, помещая их в массив strs. Далее вызывает библиотечную функцию diagonals() , которая выводит на экране "крестик". После этого на каждой итерации цикла вызывается библиотечная функция text() , которой передается очередной элемент массива; функция text() выводит на экране звездочки в количестве равному длине переданной через указатель строки. Обратите внимание на то, как подключается заголовочный файл библиотеки: через относительный адрес. Две точки обозначают переход в каталог на уровень выше, т.е. родительский по отношению к project, после чего путь продолжается во вложенный в родительский каталог library. Можно было бы указать абсолютный путь, например, "/home/sv/c/les_21/library/mylib.h". Однако при перемещении каталогов библиотеки и программы на другой компьютер или в другой каталог адрес был бы уже не верным. В случае с относительным адресом требуется лишь сохранять расположение каталогов project и library относительно друг друга. Файл main.c: #include #include "../library/mylib.h" #include "project.h" main () { rect('-',75,4); data(); rect('+',75,3); } Здесь два раза вызывается библиотечная функция rect() и один раз функция data() из другого файла проекта. Чтобы сообщить функции main() прототип data() также подключается заголовочный файл проекта. Файл project.h содержит всего одну строчку: void data (void); Из обоих файлов проекта с исходным кодом надо получить объектные файлы для объединения их потом с файлом библиотеки. Сначала мы получим исполняемый файл, содержащий статическую библиотеку, потом — связанный с динамической библиотекой. Однако с какой бы библиотекой мы не компоновали объектные файлы проекта, компилируются они как для статической, так и динамической библиотеки одинаково: gcc -c *.c При этом не забудьте сделать каталог project текущим! Компиляция проекта со статической библиотекой Теперь в каталоге project есть два объектных файла: main.o и data.o. Их надо скомпилировать в исполняемый файл project, объединив со статической библиотекой libmy1.a. Делается это с помощью такой команды: 86
gcc -o project *.o -L../library -lmy1 Начало команды должно быть понятно: опция -o указывает на то, что компилируется исполняемый файл project из объектных файлов. Помимо объектных файлов проекта в компиляции участвует и библиотека. Об этом свидетельствует вторая часть команды: -L../library -lmy1. Здесь опция -L указывает на адрес каталога, где находится библиотека, он и следует сразу за ней. После опции -l записывается имя библиотеки, при этом префикс lib и суффикс (неважно .a или .so) усекаются. Обратите внимание, что после данных опций пробел не ставится. Опцию -L можно не указывать, если библиотека располагается в стандартных для данной системы каталогах для библиотек. Например, в GNU/Linux это /lib/, /urs/lib/ и др. Запустив исполняемый файл project и выполнив программу, мы увидим на экране примерно следующее: Посмотрим размер файла project: sv@seven:/c/les_21/project$ ls -l project -rwxrwxr-x 1 sv sv 8698 2012-04-03 10:21 project Его размер равен 8698 байт. Компиляция проекта с динамической библиотекой Теперь удалим исполняемый файл и получим его уже связанным с динамической библиотекой. Команда компиляции с динамической библиотекой выглядит так: gcc -o project *.o -L../library -lmy2 -Wl,-rpath,../library/ Здесь в отличии от команды компиляции со статической библиотеки добавлены опции для линковщика: -Wl,-rpath,../library/. -Wl - это обращение к линковщику, -rpath - опция линковщика, ../library/ - значение опции. Получается, что в команде мы два раза указываем местоположение библиотеки: один раз с опцией -L, а второй раз с опцией -rpath. Видимо для того, чтобы понять, почему так следует делать, потребуется более основательно изучить процесс компиляции и компоновки программ на языке C Следует заметить, что если вы скомпилируете программу, используя приведенную команду, то исполняемый файл будет запускаться из командной строки только в том случае, если текущий каталог project. Стоит сменить каталог, будет возникать ошибка из-за того, что динамическая библиотека не будет найдена. Но если скомпилировать программу так: gcc -o project *.o -L../library -lmy2 -Wl,-rpath,/home/sv/c/les_21/library , т.е. указать для линковщика абсолютный адрес, то программа в данной системе будет запускаться из любого каталога. Размер исполняемого файла проекта, связанного с динамической библиотекой, получился равным 8604 байта. Это немного меньше, чем при компиляции проекта со статической 87
библиотекой. Если посмотреть на размеры библиотек: sv@seven:/c/les_21/library$ ls -l libmy* -rw-rw-r-- 1 sv sv 3624 2012-04-02 10:54 libmy1.a -rwxrwxr-x 1 sv sv 7871 2012-04-02 11:16 libmy2.so , то видно, что динамическая больше статической, хотя исполняемый файл проекта со статической библиотекой больше. Это доказывает, что в исполняемом файле, связанном с динамической библиотекой, присутствует лишь ссылка на нее. Задание Придумайте и реализуйте проект на языке программирования C, в котором бы использовалась вами же написанная библиотека. Урок 22. Проверочная работаВыполнение всех задания данного урока предполагает период времени выполнения более чем 1,5 часа. Поэтому если есть ограничения по времени, то оценка работы может выполняться по количеству набранных баллов. Приблизительные баллы указаны в скобках после каждого задания. 1. Напишите программу, предназначенную для наглядной демонстрации различий между такими типами данных как char, short, int, unsigned. (7) 2. Значение какого из ниже представленных массивов совпадает со строкой "Zeitgeist"? Почему? (1) arrA[9] = {'Z', 'e', 'i', 't', 'g', 'e', 'i', 's', 't'}; arrB[] = {'Z', 'e', 'i', 't', 'g', 'e', 'i', 's', 't', '\0'}; 3. Если массив arr содержит строку, то будут ли различаться значения, которые вернут функции sizeof() и strlen()? (2) 4. Напишите код, в котором одной переменной присваивается введенное пользователем число, а второй переменной модуль этого числа. После этого значение второй переменной выводится на экран. Используйте условное выражение, а не инструкцию if. (4) 5. Переделайте условное выражение из 4-го задания в макрос. (2) 6. Объявлены и определены целочисленные переменные: a = 10, b = 0, c = -3. Определите их значения после выполнения следующих двух выражений, а также значения самих выражений: (2) a-- + ++b; --c + b--; 7. Запрограммируйте вывод на экран символов так, как показано на рисунке. Используйте циклы for, а также если необходимо функцию strcat(). (12) 8. Напишите программу, которая определяет в процентном соотношении количество гласных и согласных букв в текстовом файле. Имя файла передается в программу через командную строку. (10) 9. Может ли одна ячейка памяти иметь несколько имен переменных? Может ли адрес определенной ячейки памяти содержаться во множестве переменных? Ответ поясните. (4) 10. Как изменить приведенную ниже программу так, чтобы среди параметров функции sum() не было указателя, а функция возвращала значение, оставляя тем самым возможность изменить значение любой внешней переменной? (3) #include void sum (float *base, float add); main() { 88
float a=100, b; scanf("%f", &b); sum(&a, b); printf("%.4f\n", a); } void sum (float *base, float add) { *base = *base + add/2; } 11. Напишите программу, в которой массив заполняется случайными буквами английского алфавита; значения элементов массива в символьном представлении выводятся на экран. (4) 12. Напишите функцию, которая принимает указатели на два массива. Функция должна заполнять один массив 25-ю случайными четными числами, а другой — 25 нечетными. Продемонстрируйте ее работу. (6) 13. Пользователь вводит две строки, каждая строка состоит из двух слов, разделенных пробелом. Требуется сравнить строки лексикографически, начиная со второго слова (т.е. после пробела). Напишите программу, выполняющую описанную задачу. (6) 14. Пользователь вводит адреса сайтов и их ТИЦ. Данные о каждом сайте сохраняются в элементе динамической структуры. По команде пользователя данные записываются в файл, при этом память, выделенная под динамическую структуру, высвобождается. Далее пользователь может снова начать вводить информацию о сайтах, либо завершить выполнение программы. (25) 89
Примерные решения задачУрок 1. Типы данных и их выводНапишите программу, которая выводила бы на экране данные примерно так, как на картинке. При этом используйте возможность задать ширину поля, а также выравнивание по левому и правому краям. #include main() { printf("\n |%d|\n", 555); printf(" |%10d|\n", 555); printf(" |%-10d|\n\n", 555); printf(" %-10s %7d\n", "Words:", 59); printf(" %-10s %7d\n", "Letters:", 1004); printf(" %-10s %7d\n\n", "Digits:", 8); } Напишите программу, выводящую информацию о количестве байтов, отводимых в памяти под типы данных, которые были изучены на данном уроке. #include main() { printf("\n%21s: %3lu\n", "Integer", sizeof(int)); printf("%21s: %3lu\n", "Unsigned", sizeof(unsigned)); printf("%21s: %3lu\n", "Shot", sizeof(short)); printf("%21s: %3lu\n", "Unsigned short", sizeof(unsigned short)); printf("%21s: %3lu\n", "Long", sizeof(long)); printf("%21s: %3lu\n", "Unsigned long", sizeof(unsigned long)); printf("%21s: %3lu\n", "Char", sizeof(char)); printf("%21s: %3lu\n", "Float", sizeof(float)); printf("%21s: %3lu\n", "Double", sizeof(double)); printf("%21s: %3lu\n", "Long double", sizeof(long double)); int c[10]; printf("%21s: %3lu\n", "Array of 10 integers", sizeof(c)); printf("%21s: %3lu\n\n", "String |
|
|