Как использовать указатели в прошивках на языке C
В данной статье мы обсудим операторы указателей, арифметику указателей и две ситуации, в которых указатели могут улучшить наш код.
Работа со значениями указателей
Модификация и разыменование указателей
Есть два значения, связанные с указателем. Первое – это адрес памяти, который хранится в самом указателе, а второе – это данные, которые хранятся по этому адресу памяти. Чтобы изменить адрес, сохраненный в переменной указателя, вы просто используете знак равенства.
RxByte_ptr = 0x40;
Для доступа к данным, хранящимся по адресу указателя, вы используете звездочку. Этот работает как для чтения, так и для записи.
ReceivedData = *RxByte_ptr;
*TxByte_ptr = TransmitData;
Доступ к значению, на которое ссылается указатель, называется разыменованием, а звездочка (при использовании с указателями) называется оператором разыменования.
Получение адреса переменной
Важной деталью, связанной с использованием указателей, является оператор адреса в языке C; его символом является &
. Хотя &
присоединяется к обычным переменным, а не к указателям, я всё же считаю его «оператором указателя», потому что его использование очень тесно связано с реализацией указателя.
Если перед именем переменной стоит символ &
, программа использует адрес переменной, а не ее значение.
Это позволяет вам поместить адрес переменной в указатель, даже если вы не знаете, где в памяти будет расположена конкретная переменная. Использование оператора &
демонстрируется в следующем фрагменте кода, который также служит кратким описанием базового использования указателей.
char DisplayChar;
char TestingVariable;
char *DisplayChar_ptr;
DisplayChar = 0x41;
DisplayChar_ptr = &DisplayChar;
TestingVariable = *DisplayChar_ptr;
*DisplayChar_ptr = 0x42;
TestingVariable = DisplayChar;
Рассмотрим шаг за шагом, что именно делает этот код.
DisplayChar = 0x41;
Переменная DisplayChar
сейчас хранит значение, соответствующее ASCII символу 'A'.
DisplayChar_ptr = &DisplayChar;
Указатель (DisplayChar_ptr
) теперь содержит адрес переменной DisplayChar
. Мы не знаем, что это за адрес, то есть мы не знаем число, которое хранится в DisplayChar_ptr
. Кроме того, нам не нужно это знать; это дело компилятора, а не наше.
TestingVariable = *DisplayChar_ptr;
TestingVariable
теперь содержит значение переменной DisplayChar
, а именно 0x41.
*DisplayChar_ptr = 0x42;
Мы только что использовали указатель для изменения значения, хранящегося по адресу, соответствующему переменной DisplayChar
; теперь она содержит значение 0x42, что является ASCII символом 'B'.
TestingVariable = DisplayChar;
TestingVariable
теперь содержит значение 0x42.
Арифметика указателей
В основном, переменная в языке C содержит значение, которое может изменяться, и переменные-указатели не являются исключением. Обычными арифметическими операциями, которые используются для изменения значения указателя, являются сложение (например, TxByte_ptr = TxByte_ptr + 4
), вычитание (TxByte_ptr = TxByte_ptr - 4
), инкремент/увеличение (TxByte_ptr++
) и декремент/уменьшение (TxByte_ptr--
). Можно вычесть один указатель из другого, если два указателя имеют одинаковый тип данных. Однако вы не можете добавить один указатель к другому.
Арифметика указателей не так проста, как кажется. Допустим, у вас есть указатель с типом данных long
. Вы отлаживаете некоторый код и в настоящее время выполняете пошаговую процедуру, которая многократно инкрементирует (увеличивает) этот указатель. Вы замечаете в своем окне отладки, что значение указателя с каждым шагом инкремента увеличивается не на единицу. Что тут происходит?
Если вы не можете легко дать ответ, вам следует потратить немного больше времени на размышления о природе указателей. Указатель в этом коде используется с переменными long
, то есть переменными, которые занимают в памяти четыре байта. Когда вы инкрементируете указатель, вы на самом деле не хотите, чтобы значения указателя увеличивалось на одну ячейку памяти (здесь мы предполагаем, что память организована в байтах). Скорее вы хотите, чтобы он увеличивался на четыре ячейки памяти, чтобы он указывал на следующую переменную long
. Компилятор знает это, и он изменяет значение указателя соответствующим образом.
То же самое происходит, когда вы добавляете число к указателю или вычитаете число из указателя. Адрес, сохраненный в указателе, не обязательно будет увеличиваться или уменьшаться на это число; скорее он будет увеличиваться или уменьшаться на это число, умноженное на размер типа данных указателя в байтах.
Указатели и массивы
Указатели и массивы тесно связаны. Когда вы объявляете массив, вы, по сути, создаете постоянный указатель, который всегда содержит начальный адрес массива, и индексная нотация, которую мы используем для доступа к элементам массива, также может использоваться с указателями.
Например, допустим, что у вас есть указатель с именем TxBuffer
на char
, который сейчас хранит адрес 0x30. В следующем фрагменте кода показаны два эквивалентных способа доступа по адресу 0x31.
TxByte = *(TxBuffer + 1);
TxByte = TxBuffer[1];
Когда использовать указатели
В этом разделе я хочу кратко обсудить две ситуации написания кода, которые могут выиграть от использования указателей, и которые особенно актуальны для встраиваемых приложений.
Указатель против массива
Первое естественно вытекает из обсуждения в предыдущем разделе. Указатели предоставляют альтернативный метод работы с данными, которые хранятся в виде массива. В контексте данной процедуры подход с указателями может быть более понятным или удобным.
Однако в некоторых случаях реализация на основе указателей может привести к более быстрому коду. Насколько я понимаю, это было более верно в прошлом, до того, как компиляторы стали более сложными и способными к обширной оптимизации. Тем не менее, в контексте разработки встраиваемых систем, я думаю, что всё еще есть ситуации, в которых указатели могут обеспечить незначительное увеличение скорости выполнения. Если вы действительно пытаетесь достичь минимального количества тактов, необходимого для выполнения определенной части кода, стоит попробовать указатели.
Передача указателя в функции
Широкое использование функций помогает вам писать организованный и модульный код. Это хорошо, хотя C накладывает ограничение, которое может быть в некоторых ситуациях неудобным: функция может иметь только одно возвращаемое значение. Другими словами, она может изменять только одну переменную – если только вы не используете указатели.
Эта техника работает следующим образом:
- добавьте указатель в качестве одного из входных параметров для функции;
- используйте оператор
&
, чтобы передать адрес переменной в функцию; - внутри функции адрес переменной становится значением указателя, и функция использует оператор разыменования для изменения значения исходной переменной;
- даже если исходная переменная не изменяется напрямую с помощью возвращаемого значения, код, следующий за функцией, предполагает, что значение переменной было изменено.
Вот пример:
#define STEPSIZE 3
void IncreaseCnt_and_CheckLED(char *Count)
{
*Count = *Count + STEPSIZE;
if(LED == TRUE)
return TRUE;
else
return FALSE;
}
int main()
{
char RisingEdgeCount = 0;
bit LED_State;
...
...
LED_State = IncreaseCnt_and_CheckLED(&RisingEdgeCount);
...
...
}
Заключение
Я надеюсь, что теперь у вас есть четкое представление о том, что такое указатели, и как начать работать с ними в вашем проекте прошивки на языке C. Если есть еще какие-либо аспекты встроенного ПО на C, которые вы хотели бы обсудить в будущих статьях, напишите об этом в комментариях ниже.