Это старая версия документа.


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.} 
По-настоящему сложная часть этой функции - это управление памятью. Мы используем одномерный массив (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()

 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, чтобы узнать, как инициировать генератор случайных чисел.

Elie De Brauwer - фанатик Linux из Бельгии, сейчас работает разработчиком встроенного программного обеспечения в одной из ведущих компаний по спутниковой передаче данных. Когда он не со своей семьёй, он любит играть с технологиями и проводит дни ожидая, когда Blizzard наконец выпустит Diablo III.

← К содержанию номера

← К архиву журналов