В трёх последних уроках мы плотно занимаемся адресацией данных в языке C, изучили указатели, взятие адреса, разыменование, доступ к адресам массивов, поработали также с указателями на структуры и их поля.
Теперь нам предстоит познакомиться с применением указателей на различные данные в памяти в аргументах функций. Мы узнаем, какую огромную пользу это может принести в проектировании программ.
Как устроена функция, как она объявляется и вызывается, мы уже знакомы, только до сих пор мы применяли в качестве её аргументов обычные типы данных. Оказывается, аргументами могут быть и указатели на данные.
Главная польза указателей в аргументах следующая.
Когда мы пользуемся аргументами, переданными в функцию, в её теле, то на самом деле мы используем в качестве операндов не сами данные, а их копии. Это не влияет никак на результат. Немного только тратится больше памяти. Есть в этом и плюс. Наши данные, переданные в качестве аргументов, защищены от изменения. Но представьте, что нам нужно передать в функцию огромный массив данных или переменную структуры. Тут уже затраты памяти будут очень серьёзными. Поэтому, когда нам нужно было получить доступ к таким данным в теле функций, мы их не передавали, а объявляли их глобально и просто использовали. Порой такой подход тоже не оправдан и не приветствуется. Например, нам нужно в одной функции объявить большой массив и вызвать в теле этой функции другую функцию, в которой тоже нужно воспользоваться данным массивом. Глобально объявлять такой массив мы не хотим, так как наша первая функция когда-то тоже закончит свою работу и нам хотелось бы, чтобы память, затраченная на данный массив, была очищена. Передавать такой огромный массив мы также не хотим в качестве параметра, так как это породит ещё больший расход памяти и то, что создаётся копия массива, породит также затраты процессорного времени. Тогда нам и приходит на помощь передача в качестве параметра указателя на массив. В данном случае копия массива не создаётся и с помощью этого указателя в теле вызванной функции мы получаем полный доступ к его элементам.
Вторая польза указателей в аргументах функций та, что, не создавая копию, а пользуясь самими данными в памяти, мы получаем доступ ещё на их изменение. Хотя, если нам нужно будет защитить данные от изменения, мы можем применить ключевое слово const в указателе на данные и данные также будут доступны только на чтение, при этом копия создаваться также не будет и мы сэкономим и память и время выполнения.
Указатели в аргументах функций выглядят примерно вот так, как и обычное их объявление
Вот пример объявления такой функции
void print_str(const char *c_str)
{
printf("%s\n",c_str);
}
Здесь мы передаём функции в качестве аргумента указатель на строчный массив, который потом выводим в консоль. Функции printf того и нужно, чтобы вывести строку. Ей нужен как раз указатель на неё.
А передаём мы указатель на массив, например, следующим образом.
Создаём и инициализируем строчный массив, воспользовавшись, к примеру, функцией копирования из стандартной библиотеки
char str1[30] = {};
strcpy(str1,"Hello, World!!!");
А затем мы вызываем нашу функцию, передавая ей в качестве параметра имя массива, который, как мы помним, является указателем на массив
print_str(str1);
Так же мы можем пользоваться не обязательно указателями на массив, а указателями на любые типы данных, причём мы можем не создавать переменную-указатель на тип и не привязывать её к данным, а использовать при вызове функции, имеющих указатель в параметре, адрес переменной, хранящей данные, с помощью операции взятия адреса
my_func(&data);
Думаю, теперь мы, познакомившись с тем, как именно предаются указатели на данные в аргументах функций, можем приступить к практической части.
Проект мы также создадим из проекта прошлого урока с именем MYPROG28 и присвоим ему имя MYPROG29.
Откроем наш проект в Eclipse, произведём его первоначальную настройку и удалим весь наш код из функции main() за исключением возврата. Функция main() приобретёт вот такой вид
int main()
{
return 0; //Return an integer from a function
}
Давайте пока немного разомнёмся, прежде чем применять указатели в функциях. У нас в проекте (это моя промашка) существует модуль из пары файлов ariph.h и ariph.c. Здесь есть грамматическая ошибка, хотя это никак не сказывается на работоспособности модуля. Данное имя модуля произошло от слова арифметика, что на английском пишется как Arithmetic, а не Ariphmetic. Поэтому давайте изменим букву p на t в имени модуля. Дело в том, что порой нам иногда хочется изменить имя модуля и для этого мы должны знать, как это проделать более безболезненно и быстро.
Первым делом в дереве проекта вызываем контекстное меню на имени файла и выбираем в нём пункт Rename
В открывшемся диалоге меняем имя, снимаем галку (мы сами перепишем подключения, а то как-то не совсем корректно они получаются) и соглашаемся
В файлах main.c и arith.c изменим имя в подключении заголовочного файла
#include "arith.h"
Осталось открыть Makefile и там также везде изменить ariph на arith.
Вот и всё. Попробуем собрать наш проект, если он собрался, то мы всё сделали правильно.
Ну и теперь после такой небольшой разминки начнём работать с нашим проектом.
Сначала поработаем со строковым массивом.
У нас уже есть глобальный строковый массив. перенесём его в функцию main, таким образом сделав его локальным, и проинициализируем его строкой с помощью специально-обученной функции стандартной библиотеки
1 2 3 4 |
int main() { char str1[30] = {}; strcpy(str1,"Hello, World!!!"); |
Ниже функции main() добавим функцию вывода переданной строки в виде указателя на неё в консоль. Данную функцию мы использовали в качестве примера в нашей теоретической части
1 2 3 4 5 6 |
//-------------------------------------------------------- void print_str(const char *c_str) { printf("%s\n",c_str); } //-------------------------------------------------------- |
Кроме вывода в консоль наша функция будет ещё и переходить на новую строку, то есть будет немного умнее, чем просто выводитель в консоль.
Создадим на данную функцию прототип (мы прекрасно знаем уже, как это делается, поэтому я это показывать теперь больше не буду) и вызовем её в функции main(), передав в качестве указателя в аргументе указатель на нашу строку. Как известно, имя массива уже является указателем, поэтому ещё один указатель мы создавать не будем
1 2 |
strcpy(str1,"Hello, World!!!"); print_str(str1); |
Соберём проект и запустим его на выполнение
Всё прекрасно вывелось.
Мало того, мы с помощью нашей функции можем передавать строки для вывода в консоль напрямую, не создавая заранее строковый массив, то есть писать строку прямо в аргументе функции в кавычках. Давайте это попробуем проделать
1 2 |
print_str(str1); print_str("Second string"); |
То есть строковая константа в аргументе — это уже указатель на символьный массив. Неплохо, неправда ли?
Проверим, как это сработает
Всё отлично.
Теперь добавим внизу файла подобную функцию, которая будет выводить отдельно символы нашей строки, передав также её в качестве указателя в аргументе функции
1 2 3 4 5 6 7 8 9 10 11 |
//-------------------------------------------------------- void print_chars(const char *c_str) { int i=0; while(c_str[i]) { printf("%c\n",c_str[i]); i++; } } //-------------------------------------------------------- |
В этой функции мы пробегаем по строке по её элементам в цикле, выводя их в консоль и переходя на новую строку, постоянно в условии цикла следя за тем, что у нас символ ненулевой. Как только мы встречаем нулевой символ, мы выходим из функции.
Также добавим на данную функцию прототип и вызовем её в main()
1 2 |
print_str("Second string"); print_chars(str1); |
Проверим нашу функцию в работе
Отлично.
Мы также знаем, что мы можем передавать и в эту функцию строки, не создавая массивы заранее, а писать их прямо в аргументе при вызове в кавычках.
Теперь попробуем поработать с массивом другого типа. У нас же не только строки бывают. Мы попробуем передать указатель на целочисленный массив и вывести значения его элементов в консоль также с помощью функции, в которую, мы, собственно, этот указатель и передадим.
Закомментируем в функции main() предыдущий код, объявим там целочисленный массив и сразу его проинициализируем числами
1 2 3 |
*/ unsigned int a[10] = {0x33333333, 0x44444444, 0x55555555, 0x66666666, 0x77777777, \ 0x88888888, 0x99999999, 0xAAAAAAAA, 0xBBBBBBBB, 0xCCCCCCCC}; |
Внизу файла добавим функцию, которая будет, используя указатель на такой массив, выводить его элементы в консоль. В данную функцию мы также будем с помощью обычного типа передавать длину нашего массива, так как у нас нет никакого маркера, сигнализирующего, что один из элементов массива был последним
1 2 3 4 5 6 7 8 9 10 11 12 |
//-------------------------------------------------------- void print_uint32_arr(const unsigned int *p_uint, unsigned int len) { int i=0; while(len) { printf("0x%08X\n",p_uint[i]); len--; i++; } } //-------------------------------------------------------- |
Кроме того, что мы инкрементируем индекс элемента, мы также декрементиуем его длину. Мы помним, что мы работаем не с самой переменной len и мы никак не меняем её оригинальное состояние, мы работаем с её копией. После того, как len досчитает до 0 в обратную сторону, то мы выходим из цикла.
Создадим на данную функцию прототип и вызовем её в функции main(), передав ей в качестве аргументов указатель на массив и количество элементов, которые мы хотим вывести в консоль
1 2 |
0x88888888, 0x99999999, 0xAAAAAAAA, 0xBBBBBBBB, 0xCCCCCCCC}; print_uint32_arr(a, 10); |
Посмотрим, как сработает наша функция
Всё отлично вывелось.
Теперь давайте попробуем передать в параметрах указатели на обычные данные, не на массивы.
В файле arith.c мы добавим вот такую функцию
1 2 3 4 5 6 |
//---------------------------------------------- void my_sum_p(float a_f, float b_f, float *sum_f) { *sum_f = a_f + b_f; } //---------------------------------------------- |
Наша функция будет складывать два числа, переданных в двух первых параметрах, а возвращать результат она будет уже не как обычно, а в третьем входном параметре (или аргументе), который здесь объявлен с помощью указателя на тип float. В нашей функции будет всего лишь одна команда, которая складывает параметры и присваивает их разыменованному указателю. А возвращаемого типа у нашей функции не будет. Таким образом мы с помощью указателей можем возвращать не только обычные типы, но и целые массивы.
Добавим подобную функцию для деления чисел с плавающей точкой
1 2 3 4 5 6 |
//---------------------------------------------- void my_div_p(float a_f, float b_f, float *div_f) { *div_f = a_f / b_f; } //---------------------------------------------- |
Создадим на наши функции прототипы в файле arith.h, вернёмся в функцию main() файла main.c, закомментируем там предыдущий код и добавим вот такой
1 2 3 4 5 6 |
*/ float res = .0; my_sum_p(4.44, 3.12, &res); printf("Valye sum is %f\n", res); my_div_p(10., 3., &res); printf("Valye div is %f\n", res); |
Здесь мы объявим переменную типа float и проинициализируем её пока нулём. В неё мы и будем получать результат операций. Затем с помощью одной функции вычислим сумму двух чисел, выведем результат в консоль, а затем то же самое проделаем и со второй функцией. Как видим, мы не создаём отдельный указатель на переменную, в которую мы хотим получить результат из функции. Мы используем операцию взятия адреса, с помощью которой мы и получаем указатель на данные.
Проверим, как сработают наши вызовы
Сработало всё прекрасно.
Вы можете подумать, что я забыл про наших студентов. Нет, не забыл. Мы с ними поработаем в следующей части урока, в которой мы также поработаем с указателями на структуру и на тип void.
Предыдущий урок Программирование на C Следующая часть
Смотреть ВИДЕОУРОК (нажмите на картинку)
Исходный код, если есть, скиньте пожалуйста.
Спасибо за уроки!!!