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


\

Автор: Эли Дэ Брувэр (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 цифры и знак оператора. Если оператор распознан, то вызывается функция с этими числами в качестве параметров и выводится результат.

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

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

Хотя функция 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) и утечек памяти.