В четырёх последних уроках мы плотно занимаемся адресацией данных в языке C, изучили почти всё по указателям и адресам: разыменование, доступ к адресам массивов, поработали с указателями на структуры и их поля, с указателями в аргументах функций на различные данные, с указателями типа void.
Но оказалось, что это ещё не всё. Существуют ещё указатели на функции.
Я не хотел делать такой урок, но изучив конкретно данную тему, понял, что она не менее полезна, чем те, которые мы уже изучили про указатели. И указатели на функции приносят очень интересную и нужную пользу.
Давайте для начала зададимся вопросом, откуда вообще взялся указатель на функцию. Функция для процессора — это подпрограмма, она также, как и остальные данные, располагается где-то в памяти, соответственно, данная память только для чтения, то есть мы не можем изменить код подпрограммы во время её выполнения (хотя это тоже ещё не факт, есть некоторые костыли, но нам это сейчас и не нужно и урок не о том). Вот поэтому и вопрос. А обязательно ли мы должны вызывать функцию по её имени? Ведь получали же мы данные. используя адрес их нахождения в памяти. Оказывается, тоже не обязательно. Вполне мы можем использовать и указатели. И от этого есть тоже своя польза.
Например, в C++ кроме структур существуют классы, объекты или экземпляры которых мы потом используем в программе. Как и в структурах, в классах есть поля, только, правда там они именуются свойства, а есть ещё и методы. Это такие функции, которые принадлежат данному классу и доступны только из объектов, созданных на основе него.
А у нас в C существуют структуры, в которых есть только поля. Методов у нас нет. Но, используя указатели на функции, мы вполне можем эти методы эмулировать. Пусть у нас не будет спецификаторов доступа, конструкторов и деструкторов, но со своими функциями структура станет намного интереснее, чем только с полями.
Есть ещё некоторые полезные свойства указателей на функции, но мы их затрагивать не будем на данном уроке, да они и мной ещё до конца не изучены, поработаем мы лишь со структурами, тем самым мы также поймём, как можно объявить указатель на функцию и как им воспользоваться. Ну, вернее, мы конечно же, поиграемся с нашими функциями и без структур. Всё постигается постепенно. Оно так лучше усваивается.
Давайте вообще разберёмся, как мы можем получить указатель на функцию.
Как это не удивительно, самым распространённым указателем на функцию является её имя.
Получается, что мы уже и так пользуемся указателем при вызове функции. Да и если посмотреть в дизассемблере код, когда мы вызываем подпрограмму, у нас там и есть адрес функции, а никакого имени там и нет. Тогда зачем вообще этот урок, спросите вы.
А урок затем, что указателем функции можно воспользоваться для других видов доступа к функции, так сказать, нестандартных.
Кроме указателя в виде имени функции мы можем также объявить указатель на функцию отдельной переменной
Как мы можем использовать указатель на функцию?
Классический способ такой.
Представим, что у нас есть функция, вот её прототип
void print_str(const char *c_str);
Мы объявляем переменную-указатель на функцию, например с таким именем, при этом используя строго все те же параметры, как и у оригинальной функции, на адрес которой впоследствии наш указатель будет указывать
void (*print_str_new)(const char *c_str);
У нас появилось теперь новое имя print_str_new.
Теперь мы простейшим способом присваиваем адрес оригинальной функции нашему новому указателю (а мы помним, что имя функции — это и есть указатель)
print_str_new = print_str;
И далее этим указателем мы пользуемся также, как и оригинальным именем функции
print_str_new("Hello, World!!!");
Всё оказалось просто.
Давайте теперь поиграем с указателями на функции в практическом проекте, который мы также создадим из проекта прошлого урока с именем MYPROG29 и присвоим ему имя MYPROG30.
Откроем наш проект в Eclipse, произведём его первоначальную настройку и удалим весь наш код из функции main() за исключением возврата. Функция main() приобретёт вот такой вид
int main()
{
return 0; //Return an integer from a function
}
Для начала давайте зададимся вопросом. А как мы можем узнать адрес какой-нибудь функции и вывести его, например, в консоль.
Оказывается, вот как. Раз мы знаем то, что имя функции и есть указатель на неё, то мы этим и воспользуемся.
В функции main объявим обычный указатель на целочисленный 4-байтовый тип, так как у нас адреса памяти 32-разрядных приложений именно такие
1 2 3 |
int main() { unsigned int *print_str_addr; |
И, применив приведение типа, заберём адрес функции
1 2 |
unsigned int *print_str_addr; print_str_addr = (unsigned int *)print_str; |
И теперь мы спокойно выведем его в консоль
1 2 |
print_str_addr = (unsigned int *)print_str; printf("0x%08X\n",(unsigned int)print_str_addr); |
Проверим наш код в работе
Вот таким образом мы получили адрес функции (а точнее подпрограммы, которую она породила в результате компиляции).
Давайте проверим это в отладке. В любое место функции main() установим breakpoint и выполним программу до него, перейдём в окно дизассемблирования и докрутим вниз до функции print_str
У функции действительно такой адрес.
Давайте теперь, воспользовавшись нашим указателем, попробуем вызвать с помощью него оригинальную функцию без использования её имени.
Объявим указатель на функцию
1 2 |
printf("0x%08X\n",(unsigned int)print_str_addr); void (*print_str_new)(const char *c_str); |
Присвоим ему полученный адрес функции
1 2 |
void (*print_str_new)(const char *c_str); print_str_new = (void*)print_str_addr; |
И, воспользовавшись нашим указателем, вызовем функцию
1 2 |
print_str_new = (void*)print_str_addr; print_str_new("Hello, World!!!"); |
Проверим, сработал ли данный код
Всё работает. Мы теперь можем пользоваться указателем на функцию, как самой функцией.
Закомментируем наш предыдущий код.
Оказывается, если нам не нужно узнавать значение адреса функции в числовом выражении, то можно всё проделать гораздо проще, вот таким вот образом
1 2 3 4 |
*/ void (*print_str_new)(const char *c_str); print_str_new = print_str; print_str_new("Hello, World!!!"); |
Ну в принципе мы это же и видели в теоретической части.
Проверим работу этого кода
Отлично!
Оказывается, мы можем кроме просто указателей на функции объявлять массивы таких указателей.
Зачем это нужно? Согласен, нужно редко, но может пригодиться. Поэтому не познакомить вас с такой возможностью я не имею права.
Представим, что у нас есть несколько функций с одинаковыми входящими и исходящими параметрами, но работу они делают несколько разную.
Ну давайте даже не представим, а поработаем конкретно с такими функциями.
Для этого перейдём в файл arith.c и увидим, что у нас есть две функции my_sum_p и my_div_p. Давайте создадим ещё 2 подобные функции, которые будут, соответственно, умножать и вычитать переданные параметры
1 2 3 4 5 6 7 8 9 10 11 |
//---------------------------------------------- void my_mul_p(float a_f, float b_f, float *mul_f) { *mul_f = a_f * b_f; } //---------------------------------------------- void my_sub_p(float a_f, float b_f, float *sub_f) { *sub_f = a_f - b_f; } //---------------------------------------------- |
Создадим на данные функции прототипы, вернёмся в функцию main() файла main.c, закомментируем предыдущий участок кода и создадим массив указателей на тип, подобный типу наших всех четырёх функций
1 2 |
*/ void (*arith_operations[4])(float, float, float *) = {my_sum_p, my_sub_p, my_mul_p, my_div_p}; |
Как мы знаем, в прототипах функций (при её объявлении) мы не обязаны в параметрах использовать имена, достаточно только типов. Это также касается и указателей на функции, в нашем случае массивов указателей. Вот таким образом мы и объявили массив указателей на функции одного и того же типа размером 4 элемента. В данном случае мы также используем имена функций в качестве указателей в выражениях справа.
Создадим три вещественных переменных, две — для данных, которые мы будем посылать в качестве параметров в наши арифметические функции и проинициализируем их сразу и одну для возвращения результата. Как известно, чтобы результат возвращался в аргументе, последний должен иметь тип указателя. У нас пока обычный тип, а при вызове функций мы передадим адрес данной переменной
1 2 |
void (*arith_operations[4])(float, float, float *) = {my_sum_p, my_sub_p, my_mul_p, my_div_p}; float a = 3., b = 5., res = .0; |
Вызовем в цикле по очереди наши все 4 функции, указатели на которые у нас находятся в массиве в его элементах, и в этом же цикле мы выведем результаты операций в консоль
1 2 3 4 5 6 |
float a = 3., b = 5., res = .0; for(unsigned char i=0; i<4; i++) { arith_operations[i](a, b, &res); printf("Valye oeration %hd is %f\n", i, res); } |
Проверим, как это сработает
Поставим breakpoint здесь
И теперь посмотрим, что там у нас за массив, выполнив до данной точки нашу программу в отладке.
Вот они — наши все указатели в массиве
Мы знаем адреса наших всех функций.
А в дизассемблере всё это выглядит вот так
Мы просто-напросто сохранили в стек адреса функций. Вот и весь массив. А говорят, ассемблер тяжёлый.
А в цикле потом мы эти адреса забираем
И затем вызываем функцию, находящуюся по адресу, который мы вытащили из стека
Конечно, между этими инструкциями есть ещё ряд инструкций, который нам пока не интересен, там создание и запись чисел с плавающей точкой, в которой применяются специальные команды арифметического сопроцессора.
В следующей части урока мы поработаем с массивами указателей на функции и увидим, как это порой бывает полезно. С помощью этого мы немного прокачаем наших студентов.
Предыдущий урок Программирование на C Следующая часть
Смотреть ВИДЕОУРОК в RuTub (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTub (нажмите на картинку)
Спасибо вам за огромный проделанный труд, у вас талант коротко и понятно учить. Столько всего в одном месте stm pic avr и куча железа да ещё быстро повторит С можно, просто супер сайт. Вы не думали все это собрать в книгу? немного надо только ранние статьи по STM32 переделать так как инструменты сильно поменялись за это время, например под Cube Ide. Хотя бы архив сайта создать на всякий случай чтоб такому добру не пропасть во времени
Здраствуйте!!!
А исходники можно?
Спасибо за материал!