Как использовать указатели в прошивках на языке C

Добавлено 1 июня 2019 в 03:51

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

Работа со значениями указателей

Модификация и разыменование указателей

Есть два значения, связанные с указателем. Первое – это адрес памяти, который хранится в самом указателе, а второе – это данные, которые хранятся по этому адресу памяти. Чтобы изменить адрес, сохраненный в переменной указателя, вы просто используете знак равенства.

RxByte_ptr = 0x40;

Для доступа к данным, хранящимся по адресу указателя, вы используете звездочку. Этот работает как для чтения, так и для записи.

ReceivedData = *RxByte_ptr;
*TxByte_ptr = TransmitData;

Доступ к значению, на которое ссылается указатель, называется разыменованием, а звездочка (при использовании с указателями) называется оператором разыменования.

Рисунок 1 – Разыменование указателя
Рисунок 1 – Разыменование указателя

Получение адреса переменной

Важной деталью, связанной с использованием указателей, является оператор адреса в языке C; его символом является &. Хотя & присоединяется к обычным переменным, а не к указателям, я всё же считаю его «оператором указателя», потому что его использование очень тесно связано с реализацией указателя.

Если перед именем переменной стоит символ &, программа использует адрес переменной, а не ее значение.

Рисунок 2 – Получение адреса переменной
Рисунок 2 – Получение адреса переменной

Это позволяет вам поместить адрес переменной в указатель, даже если вы не знаете, где в памяти будет расположена конкретная переменная. Использование оператора & демонстрируется в следующем фрагменте кода, который также служит кратким описанием базового использования указателей.

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. Компилятор знает это, и он изменяет значение указателя соответствующим образом.

Рисунок 3 – Изменение значения указателя при инкременте
Рисунок 3 – Изменение значения указателя при инкременте

То же самое происходит, когда вы добавляете число к указателю или вычитаете число из указателя. Адрес, сохраненный в указателе, не обязательно будет увеличиваться или уменьшаться на это число; скорее он будет увеличиваться или уменьшаться на это число, умноженное на размер типа данных указателя в байтах.

Указатели и массивы

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

Например, допустим, что у вас есть указатель с именем TxBuffer на char, который сейчас хранит адрес 0x30. В следующем фрагменте кода показаны два эквивалентных способа доступа по адресу 0x31.

TxByte = *(TxBuffer + 1);
TxByte = TxBuffer[1];

Когда использовать указатели

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

Указатель против массива

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

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

Передача указателя в функции

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

Эта техника работает следующим образом:

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

Вот пример:

#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, которые вы хотели бы обсудить в будущих статьях, напишите об этом в комментариях ниже.


На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.


Сообщить об ошибке