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


Автор: Эли Дэ Брувэр (Elie De Brauwer)

В предыдущих статьях были освещены практически все основы программирования на Си. Надеюсь, что после их прочтения вы убедились в том, что Си - не громоздкий и сложный, а очень мощный язык программирования, позволяющий выполнять множество низкоуровневых задач. Это последняя статья, в которой освещаются «основы». В последующих статьях мы сосредоточимся на практическом применении Си: на диагностировании и решении поставленных проблем в программах.

Указатели на функции

Как уже говорилось, если int a является целой переменной, то int * b=&a создаёт и инициализирует указатель b на a. Посмотрите на Листинг 1. В нём есть функция divide (строки 1-4), а на строке 6 - команда typedef, определяющая новый тип данных «mathFun». Это указатель на функцию, которая возвращает целое число и принимает два целых в качестве аргументов. В строках с 8 по 12 определяется структура, которая объединяет символ с функцией. Этот приём называется «обратный вызов» (callback) или обработчик (handler), и его используют очень часто. Этот приём может быть использован, чтобы симулировать объектно-ориентированное программирование в Си. Нужно определить структуру с некоторыми данными и указателями на функции, и в результате получится практически класс. Но наиболее часто он используется в GUI-программировании для регистрации функции, которая вызывается, когда пользователь совершает какое-либо действие. Если у вас установлен пакет manpages-dev, то набрав «man qsort» вы увидите определение функции, которая реализует алгоритм (см. Листинг 2). Как видите, эта функция способна сортировать данные и указатель на неё должен быть передан функции, которая может выполнять сравнения, что полезно при сортировке массива значений независимо от их характера. Листинг 1:

01.int divide(int a, int b)
02.{
03.    return a/b;
04.}
05.
06.typedef int (*mathFun)(int, int);
07.
08.struct operator
09.{
10.    char c;
11.    mathFun f;
12.};

Листинг 2: отрывок из man 3 qsort

NAME 
       qsort - sorts an array 
SYNOPSIS 
       #include <stdlib.h> 
 
       void qsort(void *base, size_t nmemb, size_t size, 
                  int(*compar)(const void *, const void *)); 
DESCRIPTION 
       The  qsort()  function sorts an array with nmemb elements of size size. 
       The base argument points to the start of the array.

Простой калькулятор

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

В Листинге 3 приведена простая реализация этой программы. В строке 3 происходит выделение хранилища для четырёх структур, которые заполняются оператором и указателями на функции в строках 4 по 7. Далее на строках 12-18 обрабатывается ввод пользователя. Когда пользователь что-то ввёл, в строках 20-32 производится поиск в массиве команд значения, соответствующего знаку операции. Если значение найдено, происходит обратный вызов с прочитанными данными, и выводится результат. И больше ничего делать не нужно. Листинг 3: основной цикл calc.c

01.int main()
02.{
03.    struct operator  functs[4];
04.    functs[0].c='-'; functs[0].f=&minus;
05.    functs[1].c='+'; functs[1].f=&add;
06.    functs[2].c='*'; functs[2].f=&multiply;
07.    functs[3].c='/'; functs[3].f=&divide;
08.    while(1)
09.    {
10.        int a,b,i;
11.        char c;
12.        printf("Введите a:\n");
13.        scanf("%d",&a);
14.        printf("Введите b:\n");
15.        scanf("%d",&b);
16.        printf("Введите оператор:\n");
17.        scanf("%c",&c); // Перенос строки
18.        scanf("%c",&c);
19.        i=0;
20.        while(i<4)
21.        {
22.            if(functs[i].c==c)
23.            {
24.                printf("Результат: %d\n",functs[i].f(a,b));
25.                break;
26.            }
27.            i++;
28.        }
29.        if(i==4)
30.        {
31.            printf("Неизвестный оператор: %c\n",c);
32.        }
33.    }
34.    return 0;
35. }

Ввод пользователя

Хотя функция printf() уже применялась, здесь впервые используется scanf()-подобная функция (существуют разновидности sscanf(), fscanf() и др, подобные ей). Эти функции противоположны printf() (и sprintf(), fprintf() …). Функция printf() принимает определение формата, чтобы правильно вывести переменную, и таким же образом scanf() считывает и анализирует строку, сохраняя результат в переменной. Рассмотрим строку 13. Здесь считывается «%d» (целое число) и результат сохраняется в переданный параметр. В эту функцию передаются указатели, а в printf() - переменные. На этом разница между ними заканчивается. Строки форматов совершенно одинаковы. Затруднения может вызвать только строка 17, второй вызов scanf() нужен, потому что первый получит символ переноса строки предыдущего ввода. Как и printf(), функция scanf() может принимать более сложные строки форматов для считывания нескольких переменных. Если вам интересно возвращаемое значение функции scanf(), то это количество правильно считанных переменных. Я очень советую поиграться с функциями printf(), scanf() и их параметрами, потому что они много где используются и понимать их полезно. При работе со строками очень важно помнить о количестве считанных данных. Такой простой код, как *«char s[10]; scanf(“%s\n”,s);», является прекрасным примером переполнения буфера, который обязательно используют. В подобных случаях следует ограничивать в формате количество считанных байт или использовать более продвинутые средства, например getline() (man getline), которая динамически выделит нужную память.

Упражнения

  • Допишите недостающие функции, чтобы приложение работало.
  • Измените приложение, чтобы оно работало с числами с плавающей точкой.
  • Напишите приложение, которое будет сортировать числа, используя функцию qsort().
  • Сделайте, чтобы пользователь мог выйти из приложения введя «q».
  • Измените приложение, чтобы пользователь мог вводить не символы, а фразы «5 плюс 6» или «6 минус 5». Для этого необходимо будет изменить структуры, чтобы в качестве оператора была строка, а вместо считывания символа нужно будет считывать строку. Будет прекрасно, если вы сможете написать этот код без ошибок переполнения буфера (см. man getline) и утечек памяти.
Elie De Brauwer - фанатик Linux из Бельгии. Когда он не со своей семьёй, он любит играть с технологиями и проводит дни, ожидая, когда Blizzard наконец выпустит Diablo III.