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

Описание и пояснения по указателям на функции и переменные;

Решено написать эту маленькую заметку, ибо уже несколько раз мне задавали вопрос, что это и зачем. Помню и сам не знал этого, хотя вполне себе мог писать простенькие программы.

Что такое указатели и как их готовить вы наверное уже в курсе, если нет, то быстренько объясню.

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

Объявляются указатели следующим образом:

uint16_t *pointer_ui16 = &variable;
void *pointer_Void
char *str = "Строка";

Здесь в первой строке объявляется указатель на тип uint16_t (целый без знаковый тип размером в 16 бит) и говорится что он ссылается на переменную variable. Как уже заметили есть ещё оператор &, он то как раз и возвращает адрес нашей переменной.

uint16_t variable = 0x1234; // переменная равна 0x1234 храниться по адресу 0x20001F00
uint16_t *pointer = &variable;

printf("%x", pointer);      // Здесь будет выведен адрес переменной: 0x20001F00
printf("%x", *pointer);     // Здесь будет выведено значение переменной: 0x1234
variable = 0x4678;
printf("%x", pointer);      // снова 0x20001F00
printf("%x", *pointer);     // а тут уже 0x4678

То есть, для доступа к данным на которые ссылается указатель необходимо написать знак "*". С этим вроде разобрались.

Теперь о функциях. С ними тоже не сильно сложно. Для начала пример объявления, рассмотрим всего 4:

void ( *func_name_1 )();                        // 1
void ( *func_name_2 )( void );                  // 2
void ( *func_name_3 )( uint8_t, int16_t));      // 3
uint16_t ( *func_name_4 )( uint8_t, int16_t);   // 4
  • указатель на функцию ничего не возвращающую и с неизвестным количеством и типом параметров;
  • указатель на функцию ничего не возвращающую и ничего не принимающую;
  • указатель на функцию ничего не возвращающую, но принимающую два аргумента типами uint8_t и int16_t;
  • указатель на функцию возвращающую значение типа uint16_t и принимающую два аргумента типами uint8_t и int16_t

Главное не забыть окружить скобками указатель, иначе получится не указать на функцию, а функция возвращающая указатель:

int16_t *func_1 ( void );     // функция func_1 возвращающая указатель на тип int16_t
int16_t ( *func_2 )( void );  // указатель func_2 на функцию возвращающую тип int16_t

Принцип использования можно посмотреть в примере:

// функция которая находит абсолютное значение
uint16_t usr_ABS(int16_t value)  { return (value > 0) ? (value) : (-value); }
// функция которая возводит в квадрат
uint16_t usr_POW2(int16_t value) { return value*value; }

uint16_t (*func)(int16_t);       // Указатель

int main ( void )
{
    // Присваиваем указателю адрес на функцию usr_ABS
    // Кстати говоря можно делать и так func = usr_ABS, 
    // так как просто имя функции и будет означать её адрес
    func = &usr_ABS;

    printf("%d", usr_ABS(-400)); // выводит 400 использую функцию
    printf("%d", (*func)(-400)); // выводит 400 использую указатель

    func = &usr_POW2; // а теперь указывает на usr_POW2

    printf("%d", usr_ABS(30));   // выводит 30
    printf("%d", (*func)(30));   // выводит 900
}

Получается что мы динамически изменили логику программы. Согласитесь не плохо и довольно интересно.

Как это применять вам подскажет логика и собственное чувство вкуса, самое главное теперь понятно как оно работает.

Для более глубокого понимания советую почитать книгу Брайана Кернигана и Денниса Ритчи (ссылка), либо освежить знания.