Указатели на функции в программировании на C с примерами

⚡ Умное резюме

В языке C указатели на функции хранят адреса исполняемого кода, позволяя программам косвенно вызывать функции, передавать их в качестве аргументов, создавать таблицы диспетчеризации из массивов и реализовывать функции обратного вызова, такие как процедура сравнения qsort, для гибкого и многократно используемого проектирования.

  • 🔗 Декларация: Указатель на функцию использует форму return_type (*name)(arguments); скобки вокруг имени обязательны.
  • 🔄 Обмен по адресу: Передача адресов переменных в качестве параметров-указателей позволяет функции изменять исходные значения вызывающей функции, как в примере с обменом.
  • 📦 Массивы: Имя массива уже является указателем, поэтому функции могут принимать массив или возвращать указатель на статический массив.
  • 🇧🇷 Диспетчерские таблицы: Массив указателей на функции заменяет длинные цепочки операторов switch или if, обеспечивая прямую индексацию выбранной операции.
  • 🧊 Указатели типа void: Параметр типа void * или возвращаемый тип принимает любой тип, который функция преобразует обратно перед использованием.
  • 📞 Обратные вызовы: Передача указателя на функцию в качестве аргумента обеспечивает работу функций обратного вызова, таких как функция сравнения qsort.
  • 🤖 Помощь ИИ: GitHub Copilot и ИИ-помощники формируют синтаксис указателей на функции, определения типов и сигнатуры обратных вызовов, а затем отмечают несоответствующие параметры.

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

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

Чтобы изменить фактические значения переменных, вызывающий оператор передает адреса параметрам указателя в функции.

Пример указателей функций

Например, следующая программа меняет местами два значения по два:

void swap (int *a, int *b);
int main() {
  int m = 25;
  int n = 100;
  printf("m is %d, n is %d\n", m, n);
  swap(&m, &n);
  printf("m is %d, n is %d\n", m, n);
  return 0;}
void swap (int *a, int *b) {
  int temp;
  temp = *a;
  *a = *b;
  *b = temp;}
}

Выход:

m is 25, n is 100
m is 100, n is 25

Пример указателей функций

Программа меняет местами фактические значения переменных, поскольку функция обращается к ним по адресу, используя указатель. Здесь мы обсудим процесс программы:

  1. Мы объявляем функцию, отвечающую за обмен.ping две переменные, которые принимают в качестве параметров два указателя на целочисленные значения и возвращают любое значение при вызове.
  2. В основной функции мы объявляем и инициализируем две целочисленные переменные («m» и «n»), а затем печатаем их значения соответственно.
  3. Мы вызываем функцию swap(), передавая адреса двух переменных в качестве аргументов, используя символ амперсанда. После этого мы печатаем новые замененные значения переменных.
  4. Здесь мы определяем содержимое функции swap(), которая принимает два адреса целочисленных переменных в качестве параметров и объявляет временную целочисленную переменную, используемую в качестве третьего ящика для хранения для сохранения одной из переменных значений, которые будут помещены во вторую переменную.
  5. Сохраните содержимое первой переменной, на которую указывает «а», во временную переменную.
  6. Сохраните вторую переменную, на которую указывает b, в первую переменную, на которую указывает a.
  7. Обновите вторую переменную (на которую указывает b) значением первой переменной, сохраненной во временной переменной.

Функции с параметрами массива

В C мы не можем передать массив по значению в функцию. Принимая во внимание, что имя массива является указателем (адресом), поэтому мы просто передаем имя массива функции, что означает передачу указателя на массив.

Например, рассмотрим следующую программу:

int add_array (int *a, int num_elements);
int main() {
  int Tab[5] = {100, 220, 37, 16, 98};
  printf("Total summation is %d\n", add_array(Tab, 5)); 
  return 0;}
int add_array (int *p, int size) {
  int total = 0;
  int k;
  for (k = 0; k < size; k++) {
    total += p[k];  /* it is equivalent to total +=*p ;p++; */}
 return (total);}

Выход:

 Total summation is 471

Здесь мы объясним программный код с его деталями.

Функции с параметрами массива

  1. Мы объявляем и определяем функцию add_array(), которая принимает адрес массива (указатель) с количеством его элементов в качестве параметров и возвращает общую накопленную сумму этих элементов. Указатель используется для перебора элементов массива (с использованием нотации p[k]), а сумма суммируется в локальной переменной, которая будет возвращена после перебора всего массива элементов.
  2. Мы объявляем и инициализируем целочисленный массив пятью целочисленными элементами. Мы печатаем общую сумму, передавая имя массива (которое действует как адрес) и размер массива в add_array()вызываемая функция в качестве аргументов.

Функции, возвращающие массив

В C мы можем вернуть указатель на массив, как в следующей программе:

#include <stdio.h>
int * build_array();
int main() {
  int *a;
  a = build_array(); /* get first 5 even numbers */
  for (k = 0; k < 5; k++)
    printf("%d\n", a[k]);
  return 0;}
int * build_array() {
  static int Tab[5]={1,2,3,4,5};
   return (Tab);}

Выход:

1
2
3
4
5

Здесь мы обсудим детали программы

Функции, возвращающие массив

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

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

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

Поскольку по определению мы знаем, что указатели указывают на адрес в любой ячейке памяти, они также могут указывать на начало исполняемого кода как на функции в памяти.
Указатель на функцию объявляется со знаком *, общий текст его объявления следующий:

return_type (*function_name)(arguments)

Вы должны помнить, что круглые скобки вокруг (*имя_функции) важны, потому что без них компилятор будет думать, что имя_функции возвращает указатель возвращаемого_типа.
После определения указателя функции мы должны присвоить его функции. Например, следующая программа объявляет обычную функцию, определяет указатель на функцию, присваивает указатель на функцию обычной функции и после этого вызывает функцию через указатель:

#include <stdio.h>
void Hi_function (int times); /* function */
int main() {
  void (*function_ptr)(int);  /* function pointer Declaration */
  function_ptr = Hi_function;  /* pointer assignment */
  function_ptr (3);  /* function call */
 return 0;}
void Hi_function (int times) {
  int k;
  for (k = 0; k < times; k++) printf("Hi\n");}

Выход:

Hi
Hi
Hi

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

  1. Мы определяем и объявляем стандартную функцию, которая печатает приветственный текст k раз, указанный параметром, раз при вызове функции.
  2. Мы определяем функцию-указатель (со специальным объявлением), которая принимает целочисленный параметр и ничего не возвращает.
  3. Мы инициализируем нашу функцию указателя с помощью Hi_function, что означает, что указатель указывает на Hi_function().
  4. Вместо стандартного вызова функции по taping Мы вызываем функцию с аргументами, передавая в качестве аргумента только имя функции-указателя, и всё!

Имейте в виду, что имя функции указывает на начальный адрес исполняемого кода, как имя массива, указывающее на его первый элемент. Следовательно, такие инструкции, как function_ptr = &Hi_function и (*funptr)(3), являются правильными.
ПРИМЕЧАНИЕ. Необязательно вставлять оператор адреса & и оператор косвенного обращения * во время назначения и вызова функции.

Массив указателей на функции

Массив указателей на функции может играть роль переключателя или оператора if для принятия решения, как в следующей программе:

#include <stdio.h>
int sum(int num1, int num2);
int sub(int num1, int num2);
int mult(int num1, int num2);
int div(int num1, int num2);

int main() 
{  int x, y, choice, result;
  int (*ope[4])(int, int);
  ope[0] = sum;
  ope[1] = sub;
  ope[2] = mult;
  ope[3] = div;
  printf("Enter two integer numbers: ");
  scanf("%d%d", &x, &y);
  printf("Enter 0 to sum, 1 to subtract, 2 to multiply, or 3 to divide: ");
  scanf("%d", &choice);
  result = ope[choice](x, y);
  printf("%d", result);
return 0;}

int sum(int x, int y) {return(x + y);}
int sub(int x, int y) {return(x - y);}
int mult(int x, int y) {return(x * y);}
int div(int x, int y) {if (y != 0) return (x / y); else  return 0;}
Enter two integer numbers: 13 48
Enter 0 to sum, 1 to subtract, 2 to multiply, or 3 to divide: 2
624

Здесь мы обсуждаем детали программы:

Массив указателей на функции

  1. Мы объявляем и определяем четыре Функции которые принимают два целочисленных аргумента и возвращают целочисленное значение. Эти функции выполняют сложение и вычитание.tract, умножьте и разделите два аргумента, указывающих, какая функция вызывается пользователем.
  2. Мы объявляем 4 целых числа для обработки операндов, типа операции и результата соответственно. Кроме того, мы объявляем массив из четырех указателей на функции. Каждый указатель функции элемента массива принимает два целочисленных параметра и возвращает целочисленное значение.
  3. Мы присваиваем и инициализируем каждый элемент массива уже объявленной функцией. Например, третий элемент, который является указателем третьей функции, будет указывать на функцию операции умножения.
  4. Мы ищем операнды и тип операции от пользователя, набираемого с клавиатуры.
  5. Мы вызвали соответствующий элемент массива (указатель на функцию) с аргументами и сохранили результат, сгенерированный соответствующей функцией.

Инструкция int (*ope[4])(int, int); определяет массив указателей на функции. Каждый элемент массива должен иметь одинаковые параметры и тип возвращаемого значения.
Результат оператора = ope[choice](x, y); запускает соответствующую функцию в соответствии с выбором пользователя. Два введенных целых числа являются аргументами, передаваемыми функции.

Функции, использующие указатели void

Пустые указатели используются во время объявлений функций. Мы используем тип возврата void *, позволяющий возвращать любой тип. Если мы предполагаем, что наши параметры не изменяются при переходе к функции, мы объявляем их как const.
Например:

 void * cube (const void *);

Рассмотрим следующую программу:

#include <stdio.h>
void* cube (const void* num);
int main() {
  int x, cube_int;
  x = 4;
  cube_int = cube (&x);
  printf("%d cubed is %d\n", x, cube_int);
  return 0;}

void* cube (const void *num) {
  int result;
  result = (*(int *)num) * (*(int *)num) * (*(int *)num);
  return result;}

Результат:

 4 cubed is 64

Здесь мы обсудим детали программы:

Функции, использующие указатели void

  1. Мы определяем и объявляем функцию, которая возвращает целочисленное значение и принимает адрес неизменяемой переменной без определенного типа данных. Мы вычисляем кубическое значение переменной содержимого (x), на которую указывает указатель num, и, поскольку это указатель void, нам нужно привести ее к целочисленному типу данных, используя определенный указатель нотации (* datatype), и мы возвращаем стоимость куба.
  2. Мы объявляем операнд и переменную результата. Кроме того, мы инициализируем наш операнд значением «4».
  3. Мы вызываем функцию куба, передавая адрес операнда, и обрабатываем возвращаемое значение в переменной результата.

Указатели функций как аргументы

Другой способ использовать указатель на функцию, передав его в качестве аргумента другой функции, которую иногда называют «функцией обратного вызова», поскольку принимающая функция «вызывает ее обратно».
В заголовочном файле stdlib.h функция быстрой сортировки «qsort()» использует этот метод, который представляет собой алгоритм, предназначенный для сортировки массива.

void qsort(void *base, size_t num, size_t width, int (*compare)(const void *, const void *))
  • void *base: недействительный указатель на массив.
  • size_t num: номер элемента массива.
  • size_t width Размер элемента.
  • int (*compare (const void *, const void *) : указатель функции, состоящий из двух аргументов и возвращающий 0, если аргументы имеют одинаковое значение, <0, когда arg1 предшествует arg2, и >0, когда arg1 идет после arg2.

Следующая программа сортирует массив целых чисел от малого к большому с помощью функции qsort():

#include <stdio.h>
#include <stdlib.h>
int compare (const void *, const void *); 
int main() {
  int arr[5] = {52, 14, 50, 48, 13};
  int num, width, i;
  num = sizeof(arr)/sizeof(arr[0]);
  width = sizeof(arr[0]);
  qsort((void *)arr, num, width, compare);
  for (i = 0; i < 5; i++)
    printf("%d ", arr[ i ]);
  return 0;}
int compare (const void *elem1, const void *elem2) {
  if ((*(int *)elem1) == (*(int *)elem2))  return 0;
  else if ((*(int *)elem1) < (*(int *)elem2)) return -1;
  else return 1;}

Результат:

 13 14 48 50 52

Здесь мы обсудим детали программы:

Указатели функций как аргументы

  1. Мы определяем функцию сравнения, состоящую из двух аргументов и возвращающую 0, когда аргументы имеют одинаковое значение, <0, когда arg1 предшествует arg2, и >0, когда arg1 идет после arg2. Параметры представляют собой тип указателей void, приведенный к соответствующему типу данных массива. (целое число)
  2. Мы определяем и инициализируем целочисленный массив. Размер массива хранится в Num переменная, а размер каждого элемента массива сохраняется в переменной ширины с использованием предопределенного sizeof(). C-оператор.
  3. Мы вызываем функцию qsort и передаем ей имя массива, его размер, ширину и функцию сравнения, определенную пользователем ранее, чтобы отсортировать массив в порядке возрастания. Сравнение будет выполняться путем взятия на каждой итерации двух элементов массива до тех пор, пока весь массив не будет отсортирован.
  4. Мы печатаем элементы массива, чтобы убедиться, что наш массив хорошо отсортирован, перебирая весь массив с помощью для цикла.

Часто задаваемые вопросы (FAQ)

Да. Указатель на функцию должен точно соответствовать типу возвращаемого значения и списку параметров целевой функции. Присваивание функции с другой сигнатурой является неопределенным поведением, даже если оно компилируется, поскольку вызов приведет к некорректному сохранению и считыванию аргументов во время выполнения.

typedef присваивает указателю на сложную функцию короткое имя. Написание typedef int (*)Operation)(int, int); позволяет объявить OperaФункция `tion add`, вместо повторения полного синтаксиса указателей, значительно упрощает чтение массивов, параметров и членов структур.

Указатели на функции управляют функциями обратного вызова, таблицами переходов или диспетчеризации, плагинами и системами, управляемыми событиями, конечными автоматами и библиотечными подпрограммами, такими как qsort и bsearch. OperaСистемы управления и встроенное программное обеспечение полагаются на них для выбора поведения во время выполнения без длинных цепочек условий.

Указатель данных хранит адрес переменной в памяти, а указатель функции — адрес точки входа исполняемого кода. Для запуска функции используется указатель функции, а не указатель на неё для чтения или записи данных.

Да. Указатель на функцию может быть установлен в NULL, что означает, что функция не будет назначена. Вызов функции, указанной в NULL, приводит к неопределенному поведению, поэтому перед вызовом следует использовать условие if (ptr != NULL), особенно для необязательных коллбэков.

Да. Размещение указателей на функции в качестве членов структуры позволяет объединять данные со связанными операциями, имитируя объекты и таблицы виртуальных методов. Этот шаблон лежит в основе многих библиотек C и драйверов устройств, где каждый экземпляр выбирает свою собственную реализацию функции.

Да. Искусственный интеллект-помощник в программировании может генерировать объявления указателей на функции, определения типов и сигнатуры обратных вызовов на основе подсказки на простом языке, объяснять непонятный синтаксис и выявлять несоответствие параметров или типов возвращаемых значений. Всегда компилируйте и тестируйте предложенный код, прежде чем доверять ему.

Второй пилот GitHub Предлагает синтаксис указателей на функции, дополняет тела обратных вызовов из комментария или сигнатуры и предлагает функции сравнения qsort. Откройте соответствующие заголовочные файлы, чтобы их автодополнение соответствовало объявленным вами прототипам, а затем проверьте логику.

Подведем итог этой публикации следующим образом: