Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Yuzhanin_V.V._Nizkourovnevoe_programmirovanie_mikrokontrollerov

.pdf
Скачиваний:
2
Добавлен:
12.11.2022
Размер:
1.33 Mб
Скачать

Мы объявили функцию, как и раньше, однако, теперь передаём туда не значение переменной типа int, а указатель на эту переменную. Это даёт нам возможность разыменовать указатель в теле функции и получить доступ к исходному значению переменной. Есть несколько сценариев использования передачи параметров по указателю. Один из них передача массивов. Массивы в С не могут быть переданы по значению. Чтобы передать массив в функцию, нужно использовать указатель.

void foo(int* arg);

int main() {

int bar[3] = { 1, 2, 3 }; foo(bar);

}

void foo(int* arg) {

arg[0] = arg[1] + arg[2];

}

Мы воспользовались тем, что переменная массива является указателем на первый его элемент и передали этот массив по указателю в функцию foo.

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

31

Глава 2

ПРИМЕНЕНИЕ УКАЗАТЕЛЕЙ В ЯЗЫКЕ C

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

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

(100).

void fill_cup(char cup)

{

cup += 100; // долить в кружку

}

int main()

{

char cup = 10; // кружка с напитком fill_cup(cup);

}

На первый взгляд, функция делает то, что надо – добавляет 100 к переменной cup. Однако, легко убедиться, что это не так, убедившись в отладчике, что значение cup после вызова fill_cup осталось равным 10. Если коротко, то проблема в том, что перед вызовом fill_cup создается копия переменной cup, которая, передается и затем изменяется в fill_cup, в результате так сказать

32

оригинал, т.е. переменная cup в функции main не изменяется. Детальный разбор механизма передачи входных параметров через стек в функции заслуживает отдельного изложения, здесь же зададимся вопросом, как добраться к оригиналу из функции fill_cup.

Передача параметров по ссылке в языке C

Переменные в простейшем случае просто являются поименованной ячейкой памяти, т.е. для программиста переменная выглядит как char cup, а при компиляции и исполнении машинного кода переменной cup нет, вместо нее есть ячейка в оперативной памяти процессора, расположенная по некоторому адресу. Тогда чтобы добраться к оригиналу cup из функции fill_cup, надо передать в нее не копию cup, а адрес cup.

void fill_cup(char* pointer /* pointer – по-английски указатель */)

{

/* присвоим значение по адресу памяти, который содержит указатель */

*pointer += 100;

}

int main()

{

char cup = 10; // кружка с напитком

char* pcup; // объявляем переменную-указатель pcup = ∪ /* записываем адрес переменной cup в

переменную-указатель */ fill_cup(pcup); // передаем указатель в функцию

}

Для получения адреса переменной cup сначала создается специальная переменная-указатель pcup, затем в нее записывается адрес cup, и далее указатель передается в fill_cup.

Примечание 1. Первая версия функции fill_cup использует способ передачи параметра «по значению», а при использовании указателя параметр передается «по ссылке». Термины указатель, адрес, ссылка означают практически одно и то же.

33

Примечание 2. Может возникнуть вопрос, зачем вообще так усложнять, если можно fill_cup просто сделать не void-функцией и возвращать результат обычным способом. Ответ в виде вопроса – а что делать, если функция должна возвращать несколько результатов, например, функция поиска минимума и максимума массива. Самые пытливые могут возразить про typedef struct, однако, им мы предлагаем подумать, сколько придется объявить структур, чтобы не избыточно описать результат каждой функции. И кстати, если вы знаете про struct, пора бы узнать и про указатели.

Изучим работу приведенного выше кода в отладчике.

cup = 10;

pcup = ∪

fill_cup(pcup);

cup pcup

Рис. 1. Значения переменных перед вызовом fill_cup

В окне Watch, можно увидеть адрес переменной cup в памяти

(Location = 0x045B, 1-я строка Watch).

После присвоения pcup = &cup переменная pcup будет содержать адрес переменной cup (Value для pcup совпадает с Location для cup, 2-я строка Watch). При этом отладчик учитывает, что переменная pcup является указателем и позволяет посмотреть, что лежит по адресу, на который она указывает (3-я строка

Watch).

34

Внутри функции fill_cup переменная pointer будет содержать адрес переменной cup. Для того, чтобы обратиться к ячейке памяти, на которую указывается адрес в pointer, используется унарный оператор разыменования «*», не путать с бинарным оператором «*», который означает умножение.

Пояснение.

Унарный оператор работает с одним операндом *pointer (разыменовали операнд pointer).

Бинарный оператор работает с двумя операторами a*b (умножили операнд a на операнд b).

Разыменованный указатель *pointer следует рассматривать как содержимое адреса, на который указывает указатель. Выражение *pointer += 100 означает «добавить значение 100 ячейке памяти, на которую указывает адрес переменной pointer». В нашем примере содержимое адреса – оригинал локальной переменной a функции main.

Примечание 1. Однажды разыменованный указатель не остается разыменованным все время. В дальнейшем для работы с ячейкой, на которую он ссылается, его каждый раз надо разыменовывать унарной звездочкой «*».

Примечание 2. Распространенная ошибка или даже опечатка – написать pointer += 100. В этом случае к адресу, который хранится в pointer, будет добавлено 100. Вряд ли это то, что мы хотели.

Примечание 3. Поразмыслив, можно обратить внимание, что механизм передачи параметра через стек, при котором создается копия, все равно используется. Вопрос, копия чего кладется на стек в этом случае? Указатель pa точно так же кладется на стек. Т.е. pointer есть копия pcup.

Примечание 4. Передача указателя в функцию в нашем примере получилась громоздкой. В реальном коде переменная pcup не нужна, адрес &cup можно сразу передать в fill_cup.

35

void fill_cup(char* pointer)

{

*pointer += 100;

}

int main()

{

char cup = 10; fill_cup(&cup);

}

Типизированные и нетипизированные указатели

Зададимся вопросом, как изменится код, если возникнет задача модифицировать переменную типа int (float, double, …), а не char. По аналогии напрашивается заменить char* на int* (float*, double*).

void fill_cup(int* pointer) {

*pointer += 1000; /* теперь в int можно долить больше */

}

int main() {

int cup = 10; int* pcup = ∪ fill_cup(pcup);

}

Тогда немедленно возникает вопрос, в чем разница между char* и int*? Чтобы на него ответить, обсудим более подробно, что представляет собой указатель так сказать технически. Обратите внимание, что на рис. 1 у указателя pcup тоже есть собственный адрес. Фактически, это обычная переменная, конечно, целочисленная – для адресации ячеек памяти по их порядковому номеру. Какой у нее должен быть диапазон целых значений – очевидно, этого диапазона должно быть достаточно, для адресации по всему диапазону адресов в ОЗУ данного микропроцессора. В случае ATMega16 объем ОЗУ равен 1 килобайт или 1024

36

байта. Соответственно, адрес может лежать в диапазоне 0-1023 или 0x000-0x45F. Сколько байт нужно, чтобы описать этот диапазон? Одного байта мало, двух должно быть достаточно. И действительно, sizeof(char*) равно 2. Но очевидно, указатель int* тоже должен уметь адресовать все эти же ячейки памяти, поэтому sizeof(int*), а также sizeof для других указателей, тоже даст 2.

Рис. 2. Различные виды указателей – код

*pchar *pint

Рис. 3. Различные виды указателей – карта памяти

37

Вкаких все-таки случаях проявится разница между указателями? Эту разница возникает при разыменовании указателя в коде рисунке 2. При разыменовании *pchar считается, что обращение идет к переменной char и захватывается одна однобайтная ячейка памяти. При разыменовании *pint захватываются две ячейки памяти, соответствующие типу unsigned int. Происходящее проиллюстрировано картой памяти, приведенной на рисунке 3.

Врезультате x = 0xBB, y = 0xAABB. Ожидаемо значение

x= 0xBB однобайтное, значение y = 0xAABB – двухбайтное. Неожиданно оказалось, что x = 0xBB, а не 0xAA. Дело в том, что байты a хранятся по порядку – сначала младший, затем младший.

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

Передача массивов в функцию

Для алгоритмов работы с массивами естественно учитывать переменность их длины. Соответственно, понадобится способ определения размера массива. Этот способ зависит от способа выделения памяти под массив. Для массивов, объявленных как локальные переменные внутри функции память выделяется на стеке (что бы это ни значило). В этом случае можно использовать sizeof, учтя, что объем памяти, занимаемые массивом равен в байтах количеству его элементов умножить на размер одного элемента:

int values[] = { 6, 3, 4, 5 };

int values_size = sizeof(values) / sizeof(int);

38

Попробуем этим воспользоваться. Пусть возникла необходимость реализовать функцию, которая ищет минимум в массиве:

int array_min(int values[])

{

int size = sizeof(values) / sizeof(values[0]); int result = values[0];

for (int index = 1; index < size; ++index)

{

if (values[index] < result) result = values[index];

}

return result;

}

int main()

{

int values[] = { 6, 3, 4, 5 }; int min = array_min(values);

}

Данный код, к сожалению, не работоспособен, потому что вместо массива int[] в функцию будет передан указатель int*, другими словами сигнатура функции (т.е. ее заголовок) в действительности интерпретируется компилятором следующим образом:

int array_min(int* values) ...

Отсюда ясно, что вместо объема памяти, занятой массивом, sizeof(values) выдаст размерность типа int*. Кроме того, выходит, что передача в функцию массива и указателя ничем не отличаются, поэтому принципиально и не может быть способа определить размер массива по values.

Единственный способ – передавать в функцию размер массива оттуда, где он известен:

39

int array_min(int* values, int size)

{

int result = values[0];

for (int index = 1; index < size; ++index)

{

if (values[index] < result) result = values[index];

}

return result;

}

int main()

{

int values[] = { 6, 3, 4, 5 }; int min = array_min(values,

sizeof(values) / sizeof(values[0]));

}

Зададимся вопросом, указатель на какой адрес все-таки передается в функцию? Передается указатель на нулевой элемент массива. Вызов функции:

array_min(values, ...);

эквивалентен выражению

array_min(&values[0], ...);

Разумеется, на практике используется первый способ, т.к. он проще.

Далее обратим внимание, что выражение values[index] корректно работает, несмотря на то, что values – указатель, а не массив. Другими словами, если пошагово в отладчике выполнить программу, то станет ясно, что values[0] = 6, values[1] = 3 и т.д. Это возможно благодаря работе так называемой адресной арифметики (см. ниже).

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

40