Это старая версия документа.
HOW-TO
Програма на Си. Часть 4
Автор: Эли Дэ Брувэр (Elie De Brauwer)
В четвертой статье я расскажу о важной теме, для каждого Си програм-миста, потому что она может причинить множество неприят-ностей: динамическое выделе-ние памяти (dynamic memory allocation). Непонимание этого механизма (и указателей) может привести к утечкам памяти и ошибкам в приложе-нии (например, к ошибке Segmantation Fault).
Сейчас пора каникул, и поэтому примером будет приложение, которое создаёт ASCII-снег. Чтобы создать этот эффект, я буду ис-пользовать небольшую часть биб-лиотеки, называемой «ncurses». За дополнительной информацией о библиотеке я советую посетить http://tldp.org/HOWTO/NCURSES-Programming-HOWTO, потому что я буду говорить только о функциях, которые используются в приложении.
Использование ncurses
Для использования библиотеки сначала необходимо установить пакеты ncurses:
apt-get install libncurses5 libncurses5-dev
Затем в начало исходного файла нужно добавить заголовок ncurses, дописав #include <ncurses.h>. Новым является то, что ncurses предоставляется в виде динамической библиотеки, что означает две вещи: во-первых, компоновщик должен связать наш исходный код с библиотекой ncurses. Это делается такой командой:
gcc -Wall -lncurses snow.c -o snow
Параметр -l инструктирует компоновщик добавить разделяемую библиотеку ncurses. И в результате этого мы увидим такой вывод:
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)
Запуск 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()
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.}
Листинг 2: exitfun()
1. /* При выходе правильно закрыть терминал */ 2. void exitfun() 3. { 4. endwin(); //nc 5.}
Да сделаем, что будет снег
В main() у нас есть хранилище для числа строк, столбцов и массив снежинок. Мы передаём эти три параметра в функцию updateFlakes() (Листинг 3). Если размер терминала изменён, она выделяет память. Эта функция считывает размеры терминала при каждом вызове. Если они не соответствуют хранимым в main, то выделяется новый массив, и всё начинается с начала. В строках с 6 по 19 считываются размеры и выделяется память (и освобождается занятая, если она есть). Здесь как раз и используется динамическое выделение. Иногда до компиля-ции неизвестно, сколько понадобится памяти. Здесь нам нужен один байт для каждой позиции на экране, но размер окна не фиксирован во время компиляции, поэтому необходимо это узнать и выделить нужное количество памяти. То же самое происходит при изменении размеров окна, когда нужно обновить количество необходимой памяти. Для этого исполь-зуются функции malloc() (строка 15) и free() (строка 13). Функции malloc() (что означает memory allocate - выделить память) нужно передать количество байт, которые нужно выделить, и она возвращает указатель на это количество байт (или NULL, если память закончилась). Функция free() информирует систе-му, что память больше не нужна. Неправильное соче-тание malloc() и free() приве-дёт к утечкам памяти и, в конце концов, к краху при-ложения. Ну, и это всё. Просто, не так ли? Теперь становится понятно, сколько можно создать себе проблем, используя динамическое выделение памяти?
Листинг 3: updateFlakes
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.}
Да будет снег
Наконец, нужно просто перебрать массив и вывес-ти снег на экран. Это по-казано в Листинге 4. Это лишь два for-цикла: один - для строк, другой - для столбцов; затем принять решение выводить снежинку или нет.
Заключение
Уже было освещено много «сложного материала», хотя пред-ставлено всего четыре статьи. Можно увидеть, что в этой статье мы уже отходим от общего программирования на С и смеща-емся к приложениям более специ-фичным для Linux/Ubuntu. Целью этих статей является продолжение этого и всё большее обращение к харак-терным для Linux приёмам программирования, и на этом я желаю всем вам, энту-зиастам, счастливого Нового Года, полного открытий.
Листинг 4: drawScreen()
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.}
Упражнения:
- Заставьте приложение работать на вашей системе (необ-ходимо выяснить нужные заголовки, подсказка: обратитесь к man-страницам вызовов, которые выдают ясные ошибки).
- Вместо передачи exitfun() в atexit() можно сразу передать endwin(); проверьте, что это работает. Прочитайте man-страницу. Прототипы каких функ-ций она принимает. Почему нет смысла передавать функции, не возвращающие значения.
- Отключите повторное выделение памяти для field. Попробуйте теперь изменить размер окна. Есть ли плюсы?
- Заметьте, что используемый массив field не освобождается функцией free() при выходе. Это не создаст проблем, не приведёт к утечке памяти, и ядро освободит память. Сделайте field глобальной переменной (поместив вне main()) и освободите память при выходе.
- Напишите приложение с кодом while(1){malloc(1);}, чтобы подтвердить, что память в конце концов закончится.
- Изучите man-страницы random и srand, чтобы узнать, как инициировать генератор случайных чисел.