Различия
Здесь показаны различия между двумя версиями данной страницы.
Предыдущая версия справа и слева Предыдущая версия Следующая версия | Предыдущая версия | ||
fullcircle:fcm-ru-20:программа_на_си_ч4 [2010/03/30 20:40] |
— (текущий) | ||
---|---|---|---|
Строка 1: | Строка 1: | ||
- | ======HOW-TO====== | ||
- | =====Програма на Си. Часть 4===== | ||
- | ===Автор: Эли Дэ Брувэр (Elie De Brauwer)=== | ||
- | В четвертой статье я расскажу о важной теме, для каждого Си програм-миста, потому что она может причинить множество неприят-ностей: динамическое выделе-ние памяти (dynamic memory allocation). Непонимание этого механизма (и указателей) может привести к утечкам памяти и ошибкам в приложе-нии (например, к ошибке Segmantation Fault). | ||
- | |||
- | Сейчас пора каникул, и поэтому примером будет приложение, которое создаёт ASCII-снег. Чтобы создать этот эффект, я буду ис-пользовать небольшую часть биб-лиотеки, называемой "ncurses". За дополнительной информацией о библиотеке я советую посетить [[http://tldp.org/HOWTO/NCURSES-Programming-HOWTO|http://tldp.org/HOWTO/NCURSES-Programming-HOWTO]], потому что я буду говорить только о функциях, которые используются в приложении. | ||
- | |||
- | ===Использование ncurses=== | ||
- | Для использования библиотеки сначала необходимо установить пакеты ncurses: | ||
- | <code>apt-get install libncurses5 libncurses5-dev</code> | ||
- | Затем в начало исходного файла нужно добавить заголовок ncurses, дописав **#include <ncurses.h>**. Новым является то, что ncurses предоставляется в виде динамической библиотеки, что означает две вещи: во-первых, компоновщик должен связать наш исходный код с библиотекой ncurses. Это делается такой командой: | ||
- | <code>gcc -Wall -lncurses snow.c -o snow</code> | ||
- | Параметр **-l** инструктирует компоновщик добавить разделяемую библиотеку ncurses. И в результате этого мы увидим такой вывод: | ||
- | <code>edb@lapedb:~/fullcircle/c-4$ ldd snow* | ||
- | linux-gate.so.1 => (0xb805c000)* | ||
- | libncurses.so.5 => /lib/libncurses.so.5 (0xb7ff7000)* | ||
- | libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7e99000)* | ||
- | libdl.so.2 => /lib/tls/i686/cmov/libdl.so.2 (0xb7e94000)* | ||
- | /lib/ld-linux.so.2 (0xb8042000)</code> | ||
- | |||
- | Запуск ldd показывает, что приложению требуется наличие в нашей системе библиотеки libncurses.so.5. Ещё это значит, что не удастся запустить этот бинарный файл в системе, где нет этой библиотеки. | ||
- | Что может ncurses? На самом деле, текстовый терминал - это странная вещь. Используя printf(), можно вывести текст, но он всегда появляется в конце строки. Нельзя перематывать назад, использовать цвета, печатать жирные символы и т.д. Также существуют "управляющие последователь-ности" (escape sequences), которые влияют на поведение курсора и вывод текста в таких терминалах (они существуют с самого начала истории компью-теров). Но такие последователь-ности не удобны для человека. Итак, ncurses - это своего рода оболочка, которая облегчает использование управляющих последовательностей. В коде примера я добавил %%//nc%% после вызова функций, которые относятся к ncurses. Вот функции, которые я использовал: | ||
- | * getmaxyx() возвращает размеры терминала | ||
- | * clear() отчищает экран | ||
- | * mvaddch() отображает символ в заданном месте | ||
- | * refresh() делает принудительный вывод в терминал | ||
- | * endwin() правильно завершает работу терминала при выходе | ||
- | * initscr() инициализирует библиотеку ncurses | ||
- | |||
- | ===Функция main=== | ||
- | |||
- | Функция main делает немного (см. //Листинг 1//). Она инициализирует экран (строка 6) и каждую секунду обновляет массив снежинок (строка 12). Здесь только одна особенность - функция atexit(). Она используется, чтобы инструк-тировать приложение, что перед выходом нужно вызвать эту функцию. Её код дан в //Листинге 2//. Она просто вызывает endwin(). Заметьте, что здесь используется приём, называемый "указатель на функцию" (function pointer). Мы можем использовать указатели на фукции так же, как и на данные, и это просто имя функции без скобок. | ||
- | |||
- | //**Листинг 1: main()**// | ||
- | <code c> | ||
- | 1.int main() | ||
- | 2.{ | ||
- | 3. char * field=NULL; | ||
- | 4. int row=0; | ||
- | 5. int col=0; | ||
- | 6. initscr(); //nc | ||
- | 7. atexit(exitfun); | ||
- | 8. | ||
- | 9. /* Вечный снегопад ! */ | ||
- | 10. while(1) | ||
- | 11. { | ||
- | 12. updateFlakes(&field,&row,&col); | ||
- | 13. if(field==NULL) | ||
- | 14. { | ||
- | 15. break; | ||
- | 16. } | ||
- | 17. drawScreen(field,row,col); | ||
- | 18. sleep(1); | ||
- | 19. } | ||
- | 20. return 0; | ||
- | 21.} | ||
- | </code> | ||
- | |||
- | //**Листинг 2: exitfun()**// | ||
- | <code c> | ||
- | 1. /* При выходе правильно закрыть терминал */ | ||
- | 2. void exitfun() | ||
- | 3. { | ||
- | 4. endwin(); //nc | ||
- | 5.} | ||
- | </code> | ||
- | |||
- | ===Да сделаем, что будет снег=== | ||
- | |||
- | В main() у нас есть хранилище для числа строк, столбцов и массив снежинок. Мы передаём эти три параметра в функцию updateFlakes() (Листинг 3). Если размер терминала изменён, она выделяет память. Эта функция считывает размеры терминала при каждом вызове. Если они не соответствуют хранимым в main, то выделяется новый массив, и всё начинается с начала. В строках с 6 по 19 считываются размеры и выделяется память (и освобождается занятая, если она есть). Здесь как раз и используется динамическое выделение. Иногда до компиля-ции неизвестно, сколько понадобится памяти. Здесь нам нужен один байт для каждой позиции на экране, но размер окна не фиксирован во время компиляции, поэтому необходимо это узнать и выделить нужное количество памяти. То же самое происходит при изменении размеров окна, когда нужно обновить количество необходимой памяти. Для этого исполь-зуются функции malloc() (строка 15) и free() (строка 13). Функции malloc() (что означает memory allocate - выделить память) нужно передать количество байт, которые нужно выделить, и она возвращает указатель на это количество байт (или NULL, если память закончилась). Функция free() информирует систе-му, что память больше не нужна. Неправильное соче-тание malloc() и free() приве-дёт к утечкам памяти и, в конце концов, к краху при-ложения. Ну, и это всё. Просто, не так ли? Теперь становится понятно, сколько можно создать себе проблем, используя динамическое выделение памяти? | ||
- | |||
- | //**Листинг 3: updateFlakes**// | ||
- | <code c> | ||
- | 1./* Обновим стурктуру */ | ||
- | 2.void updateFlakes(char ** fieldIn, int *rowIn, int *colIn) | ||
- | 3.{ | ||
- | 4. int numnew=0; int row=0; int col=0; int i=0; | ||
- | 5. char *field=*fieldIn; | ||
- | 6. getmaxyx(stdscr,row,col); //nc | ||
- | 7. | ||
- | 8. /* Создадим новый field */ | ||
- | 9. if(field==NULL || *rowIn!=row || *colIn!=col) | ||
- | 10. { | ||
- | 11. if(field!=NULL) | ||
- | 12. { | ||
- | 13. free(field); | ||
- | 14. } | ||
- | 15. *fieldIn=malloc(row*col); | ||
- | 16. field=*fieldIn; | ||
- | 17. memset(field,0,row*col); | ||
- | 18. *rowIn=row; *colIn=col; | ||
- | 19. } | ||
- | 20. | ||
- | 21. /* Применим гравитацию ! */ | ||
- | 22. memmove(&field[col],&field[0],(row-1)*col); | ||
- | 23. memset(field,0,col); | ||
- | 24. numnew=random()%(col/2); | ||
- | 25. for(i=0;i<numnew;i++) | ||
- | 26. { | ||
- | 27. field[random()%col]=1; | ||
- | 28. } | ||
- | 29.} | ||
- | </code> | ||
- | |||
- | |{{:fullcircle:20:memmove.png|}}|По-настоящему сложная часть этой функции - это управление памятью. Мы используем одномерный массив (char* field) для хранения двумерных дан-ных (две стороны экрана). Проще говоря, это означает, field[0] соответствует строке 0, столбцу 0; field[1] - сроке 0, столбцу 1; field[row] - сроке 1, столбцу 0, а field[row+1] - строке 1, столбцу 1. Легче работать с одним большим массивом, чем с массивом массивов. На Рисунке 1 показана эта схема для экрана размером в 5 строк и 3 столбца. Мы используем memset() (строка 17), что бы заполнить выделенный массив нулями (это всегда хорошая идея, потому что выделенная память обычно занята мусором). Однако магия происхо-дит в строке 22 в функции memmove(), которая передвигает первые row-1 строк на col байт. На Рисунке 1 этот сдвиг изоб-ражен пунктирной стрел-кой. Когда это сделано, мы присваиваем нули новой "первой" строке и меняем случайные ячейки на 1 (это будет снег).| | ||
- | |||
- | ===Да будет снег=== | ||
- | Наконец, нужно просто перебрать массив и вывес-ти снег на экран. Это по-казано в //Листинге 4//. Это лишь два for-цикла: один - для строк, другой - для столбцов; затем принять решение выводить снежинку или нет. | ||
- | |||
- | ===Заключение=== | ||
- | Уже было освещено много "сложного материала", хотя пред-ставлено всего четыре статьи. Можно увидеть, что в этой статье мы уже отходим от общего программирования на С и смеща-емся к приложениям более специ-фичным для Linux/Ubuntu. Целью этих статей является продолжение этого и всё большее обращение к харак-терным для Linux приёмам программирования, и на этом я желаю всем вам, энту-зиастам, счастливого Нового Года, полного открытий. | ||
- | |||
- | //**Листинг 4: drawScreen()**// | ||
- | <code c> | ||
- | 1./* Да будет снег */ | ||
- | 2.void drawScreen(char * field, int row, int col) | ||
- | 3.{ | ||
- | 4. clear(); //nc | ||
- | 5. int x=0; | ||
- | 6. int y=0; | ||
- | 7. for(y=0;y<row;y++) | ||
- | 8. { | ||
- | 9. for(x=0;x<col;x++) | ||
- | 10. { | ||
- | 11. if(field[y*col+x]==1) | ||
- | 12. { | ||
- | 13. mvaddch(y,x,'*'); //nc | ||
- | 14. } | ||
- | 15. } | ||
- | 16. } | ||
- | 17. refresh(); //nc | ||
- | 18.} | ||
- | </code> | ||
- | |||
- | ===Упражнения:=== | ||
- | * Заставьте приложение работать на вашей системе (необ-ходимо выяснить нужные заголовки, подсказка: обратитесь к man-страницам вызовов, которые выдают ясные ошибки). | ||
- | * Вместо передачи exitfun() в atexit() можно сразу передать endwin(); проверьте, что это работает. Прочитайте man-страницу. Прототипы каких функ-ций она принимает. Почему нет смысла передавать функции, не возвращающие значения. | ||
- | * Отключите повторное выделение памяти для field. Попробуйте теперь изменить размер окна. Есть ли плюсы? | ||
- | * Заметьте, что используемый массив field не освобождается функцией free() при выходе. Это не создаст проблем, не приведёт к утечке памяти, и ядро освободит память. Сделайте field глобальной переменной (поместив вне main()) и освободите память при выходе. | ||
- | * Напишите приложение с кодом while(1){malloc(1);}, чтобы подтвердить, что память в конце концов закончится. | ||
- | * Изучите man-страницы random и srand, чтобы узнать, как инициировать генератор случайных чисел. | ||
- | |||
- | {{ :fullcircle:20:snow.png?500 }} | ||
- | <note important>Elie De Brauwer - фанатик Linux из Бельгии, сейчас работает разработчиком встроенного программного обеспечения в одной из ведущих компаний по спутниковой передаче данных. Когда он не со своей семьёй, он любит играть с технологиями и проводит дни ожидая, когда Blizzard наконец выпустит Diablo III.</note> | ||
- | |||
- | [[http://help.ubuntu.ru/fullcircle/fcm-ru-20|К содержанию номера]] | ||
- | |||
- | [[http://help.ubuntu.ru/fullcircle|К архиву журналов]] | ||
- | |||
- | {{tag>Си Програмирование Full_Circle}} |