Как вывести указатель в си
Указатель в языке Си
Указатель — переменная, содержащая адрес объекта. Указатель не несет информации о содержимом объекта, а содержит сведения о том, где размещен объект.
Указатели широко используются в программировании на языке Си.
Указатели часто используются при работе с массивами.
Память компьютера можно представить в виде последовательности пронумерованных однобайтовых ячеек, с которыми можно работать по отдельности или блоками.
Каждая переменная в памяти имеет свой адрес — номер первой ячейки, где она расположена, а также свое значение. Указатель — это тоже переменная, которая размещается в памяти. Она тоже имеет адрес, а ее значение является адресом некоторой другой переменной. Переменная, объявленная как указатель, занимает 4 байта в оперативной памяти (в случае 32-битной версии компилятора).
Указатель, как и любая переменная, должен быть объявлен.
Общая форма объявления указателя
Тип указателя — это тип переменной, адрес которой он содержит.
Для работы с указателями в Си определены две операции:
Для указанного примера обращение к одним и тем же значениям переменной и адреса представлено в таблице
Расположение в памяти переменной a и указателя b:
Необходимо помнить, что компиляторы высокого уровня поддерживают прямой способ адресации: младший байт хранится в ячейке, имеющей младший адрес.
Комментариев к записи: 80
// Функция кодирования текста
uint32_t* encrypt(uint32_t* v, uint32_t* k)
<
uint32_t v0 = v[0];
uint32_t v1 = v[1];
uint32_t sum = 0;
/* a key schedule constant */
uint32_t delta = 0x9e3779b9;
/* cache key */
uint32_t k0 = k[0];
uint32_t k1 = k[1];
uint32_t k2 = k[2];
uint32_t k3 = k[3];
uint32_t i;
/* basic cycle start */
for (i = 0; i 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1);
v1 += ((v0 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3);
>
/* end cycle */
return v; // Возвращаем указатель на нулевой элемент массива зашифрованного числа
// Функция декодирования текста
uint32_t* decrypt(uint32_t* v, uint32_t* k)
<
/* set up */
uint32_t v0 = v[0];
uint32_t v1 = v[1];
uint32_t sum = 0xC6EF3720;
uint32_t i;
/* a key schedule constant */
uint32_t delta = 0x9e3779b9;
/* cache key */
uint32_t k0 = k[0];
uint32_t k1 = k[1];
uint32_t k2 = k[2];
uint32_t k3 = k[3];
uint32_t* plain;
char shelf1[8]; // В массив записан текст из 8-символов
char shelf2[8];
plain = (uint32_t*)shelf1; // Загружаем текст в plain
uint32_t* encoded = encrypt(plain, key); // Шифруем текст
uint32_t* decoded = decrypt(plain, key); // Расшифровываем текст
uint32_t* decrypt(uint32_t* v, uint32_t* k)
<
/* set up */
uint32_t v0 = v[0];
uint32_t v1 = v[1];
%ls pointers.c:14:66: warning: format ‘%x’ expects argument of type ‘unsigned int’, but argument 2 has type ‘int *’ [-Wformat=] printf(«\n Значение указателя b равно %x шестн.», b);
^ %ls pointers.c:15:85: warning: format ‘%x’ expects argument of type ‘unsigned int’, but argument 2 has type ‘int **’ [-Wformat=] ntf(«\n Адрес расположения указателя b равен %x шестн.», &b);
#include
#include
#include
void helloWorld (GtkWidget *wid, GtkWidget *win)
<
GtkWidget *dialog = NULL ;
dialog = gtk_message_dialog_new (GTK_WINDOW (win), GTK_DIALOG_MODAL,
GTK_MESSAGE_INFO, GTK_BUTTONS_CLOSE, sqlite3_libversion());
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_CENTER);
gtk_dialog_run (GTK_DIALOG (dialog));
gtk_widget_destroy (dialog);
>
int main ( int argc, char *argv[])
<
GtkWidget *button = NULL ;
GtkWidget *win = NULL ;
GtkWidget *vbox = NULL ;
/* Create a vertical box with buttons */
vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
gtk_container_add (GTK_CONTAINER (win), vbox);
/* Enter the main loop */
gtk_widget_show_all (win);
gtk_main ();
return 0;
>
Указатели в языке Си
Тема указателей довольно сложна и мне часто присылают вопросы по ней. В этой статье мы подробно разберем тему указателей.
Рекомендуемая работа с памятью
Перед тем, как разбираться с указателями, сначала посмотрим, как лучше работать с памятью в Си.
Рекомендуемый способ — это прямо описать ту область памяти, которую вы хотите использовать.
В этом примере мы задали целое число, массив и вещественное число. То есть мы явно дали имя элементу данных.
Если мы теперь будем использовать этот элемент данных, то достаточно указать его имя.
Почему рекомендуется делать именно так? Три причины:
Причина 1. Соблюдение принципа изоляции кода
В языке Си данные в функцию передаются по значению. Эти значит, что копия входных параметров размещается на стеке. После выхода из функции эти данные уничтожаются. Поэтому программист уверен, что никакого влияния на остальную программу эти данные не окажут.
Причина 2. Простой вызов в отладчике
Просто добавьте имя элемента данных и вы можете легко наблюдать, что с ним происходит во время отладки.
Причина 3. Самодокументируемый код
Если мы дадим объектам данных осмысленные имена, то код будет хорошо читаться. Например:
Но так как язык Си универсален и есть много способов писать программы, то есть и такой способ как указатели.
Что такое указатели?
Указатель — это переменная, которая содержит адрес некоторого объекта в памяти.
Для работы с указателями используются два оператора:
& — получить адрес переменной (&x — адрес переменной x)
* — получить значение переменной по адресу (*px — значение по адресу px)
Рассмотрим участок памяти. Предположим, что по адресу 54100 размещена символьная переменная char x;
При заведения указателя мы сразу говорим компилятору, что мы завели указатель на объект типа char. Чтобы не было путаницы в именах рекомендуется указатель начинать с символа «p».
Важный момент. Когда комплятор выделяет память под «char x», то выделяется один байт, потому что x — это символ, то есть это однобайтовая переменная. Но когда компилятор выделяет память под «char *px», то выделяется обычно 4 байта, так как адрес (в 32-х битовой системе) занимает 4 байта.
*px — читается как «взять значение по адресу, xранящемуся в px»
Теперь нам нужно записать:
Для этого мы пишем следующие строки:
После этого мы можем работать с этим адресом в памяти как через имя переменной, так и через указатель. Получение значение через указатель называется разыменование указателя.
Но тут нужно быть осторожным. Если в указателе лежит адрес, который не выделен программе, то эта ситуация называется висячий указатель.
Предположим, что px — это висячий указатель. Действия с самим указателем px могут быть любыми, программа не пострадает. Но если мы выполним действие *px над памятью, которая не была выделена программе, то операционная система прекратит действие программы и напишет что-то вроде: «Программа выполнила недопустимую операцию и будет закрыта».
Преимущество указателей
Преимущество указателей в том, что они позволяют передать в функцию значение по ссылке. Слово «ссылка» означает, что мы не передаем значение, а ссылаемся на адрес этого значения. В этом случае можно внутри функции изменять значение элемента данных. Хотя указатель и будет уничтожен после выхода из функции, но мы изменили значение памяти по указателю и это изменение значения сохранится после выход из функции.
Именно поэтому их часто используют.
Недостатки указателей
Главные недостатки указателей:
1. Нарушение принципов изоляции кода
Ошибка в указателе может привести к тому, чтобы будет испорчена память в случайном месте. Хорошо еще, если повезет и программа рухнет, тогда программист сразу заметит ошибку. Но если программа продолжит работу, то найти ошибку будет очень сложно, ведь она не сразу проявляется.
2. Отвлечение внимание на детали реализации
При использовании указателей программисту нужно держать в уме принципы работы с памятью, а это отвлекает от сути задачи, которая решает программист. При правильном подходе к программированию программист должен думать только решаемой задаче, и не отвлекаться на посторонние детали.
3. Плохая читаемость кода
Прямое использование переменной является самоочевидной вещью. Если мы видим x++, то сразу понимаем, что происходит, а вот если мы видим (*px)++ или *px++, то чтобы понять процесс, нужно вдумываться.
Сравним два варианта кода. Код с переменными:
и код с указателями
Мы видим, что с указателями читаемость кода хуже, а преимуществ никаких мы не получили.
Указатели лучше вообще не использовать.
Конечно же, на это последует вопрос: «А как тогда изменять значения внутри функции?»
Этот код поменяет значения любых элементов массива.
Если же речь идет именно о переменных, то правильный ответ такой: если вам нужно изменять внешние значения переменных внутри функции — у вас неверно спроектирована программа.
В правильно спроектированной программе есть три вида элемента данных:
Для указателей при таком подходе места нет. Но так как указатели широко используются в различных библиотеках, то работать с ними надо уметь.
Ответы на вопросы
Вопрос
Мы сначала передали адрес a в функцию AddFive, затем создали указатель int px(но почему именно в аргументе функции?), далее значение по адресу указателя увеличили на 5. Но тут непонятно, разве так будет работать? То есть, нужно сначала адрес присвоить указателю, как Вы показывали ранее в статье. Получится вот так:
Ответ
В аргументах функции ничего не создается. В аргументах указываются типы, чтобы компилятор мог проверить их при вызове функции. Адрес указателю px не надо присваивать внутри функции, так как этот адрес уже передан в качестве аргумента при вызове функции. То есть во время работы функции указатель px уже указывает на нужный адрес.
Вопрос
Я проверил код в CodeBlocks. Если мы укажем, например, вот так:
то возникнет ошибка: «px undeclared». Т.е. как видите указатель px не объявлен. А чтобы всё работало мы одновременно, c одной стороны, объявляем указатель в аргументе функции AddFive, а с другой стороны, записываем в указатель адрес a. Поэтому, непонятно почему Вы считаете, что ничего там не создаётся. Ведь память под указатель выделилась, так? И как раз, так как мы создали указатель, пусть и в аргументе функции, программа и работает.
Ответ
На этапе компиляции программы при проверке аргументов функции компилятор ничего не создает, а только проверяет соответствие типов аргументов в описании функции и при ее вызове. В данном примере в описании функции нет аргументов, а при вызове передается адрес — это первая ошибка в данном фрагменте. Вторая ошибка заключается в том, что идет обращение к переменной px, но она не объявлена, поэтому компилятор пишет, что «px undeclared», то есть «переменная px не объявлена».
Когда ошибки будут устранены и программа будет запущена, то в момент вызова AddFive(&a) произойдет следующее:
То есть в данном примере память для указателя специально не создается, а используется обычный стек для аргументов функции.
Забегая вперёд скажу, указатель это очень странный предмет простая вещь, вообще в языке СИ нет ничего проще чем указатель, и в тоже время это пожалуй самый мощный инструмент, с помощью которого можно творить великие дела . Однако многие падаваны не понимают что это такое, поэтому я попробую внести свою лепту.
Для лучшего понимания, вначале мы разберёмся с «обычными» переменными (впрочем указатель, это тоже обычная переменная, но пока мы условно разделим эти понятия).
Переменная
Итак, у нас есть переменные char, uint16_t, uint32_t, и прочие. Всё это «типизированные» переменные, то есть переменные хранящие определённый тип данных. Переменная char (8 бит) хранит однобайтовое число/символ, uint16_t (16 бит) хранит двухбайтовое число, и uint32_t (32 бита) хранит четырёхбайтовое число.
Теперь разберёмся что значит «переменная хранит» и как это вообще выглядит внутри «железа». Напомню, что бы мы не делали в компьютере или микроконтроллере, мы всего лишь оперируем значениями в ячейках памяти (ну или регистрами в случае с микроконтроллером).
Предположим что мы объявили и инициализировали (то есть записали в них значения) две переменные…
Что это за переменные, глобальные или нет значения не имеет, пускай будут глобальными.
Представим себе небольшой кусочек памяти компьютера где-то ближе к началу…
Клетки это ячейки памяти, а цифры это номера ячеек, то есть адреса. В каждой ячейке может храниться один байт данных (8 бит). Когда мы хотим обратится к тем или иным данным находящимся в памяти, мы обращаемся к ним по нужным нам адресам.
Если мы захотим получить не содержимое ячейки, а её адрес, то для этого есть специальная конструкция, но об этом позже.
Я специально подчеркнул две фразы ибо в них кроется ключевой смысл отличающий «обычную» переменную от указателя, поэтому повторю — когда мы обращаемся к имени «обычной» переменной, мы обращаемся непосредственно к содержимому ячейки/ячеек. То есть мы оперируем именно данными, хранящимися в этой ячейке/ячейках.
Указатель
Наконец пришло время дать определение указателю.
Указатель объявляется так же как и «обычная» переменная, с той лишь разницей, что перед именем ставиться звёздочка…
Тут стоит отметить, что при использовании указателя, звёздочка выступает в двух ипостасях, первая это как сейчас, при объявлении, а про вторую мы узнаем позже.
И да, звездочку можно ставить как угодно…
Теперь увеличим наш кусочек памяти на две ячейки, чтобы было удобнее…
… и рассмотрим что же произойдёт внутри системы после объявления указателя.
Компилятор выделил в памяти четыре ячейки для указателя, например 5681, 5682, 5683 и 5684 (см. ниже).
Размер указателя в современных компьютерах бывает либо 32-ух битный (4 байта), либо 64-ёх битный (8 байт), так как он должен хранить в себе какой-то адрес памяти, к которому мы будем обращаться через этот указатель.
Таким образом в первом случае мы можем адресовать (обратится по адресу) до 4Гб, а во втором свыше восемнадцати квинтиллионов байт . Если бы указатель был меньшей разрядности, например 16-ти битный, то не смог бы хранить в себе адреса выше 65535.
Размер указателя связан отчасти с разрядностью ОС, отчасти с шиной данных, отчасти от режима компилятора (эмуляция 32-ух битных программ на 64-ёх битных системах), и ещё чёрт знает от чего, нам это совершенно не важно.
И так же как и в случае с «обычными» переменными, компилятор ассоциировал имя ptr с этими четырьмя ячейками, и произошла инициализация указателя нулём.
NULL это дефайн из хедера стандартной библиотеки stdio…
В результате мы создали указатель, который хранит адрес нулевой ячейки памяти, то есть указывает на нулевую ячейку памяти…
Зачем же мы инициализировали наш указатель нулём, ведь обращение к нулевому адресу привело бы к мгновенному падению программы? Всё очень просто, давайте представим что мы объявили указатель без инициализации.
Тогда в ячейках 5681, 5682, 5683 и 5684 скорее всего оказался бы какой-то «мусор» (какие-то бессмысленные цифры), и если бы мы в дальнейшем забыли присвоить указателю какой-то конкретный, нужный нам, адрес, и потом обратились бы к этому указателю, то скорее всего «мусор» оказался бы каким-то адресом, и мы сами того не зная случайно что-то сделали с хранящимися по этому адресу данными. Во что бы это вылилось неизвестно, скорее всего программа не упала бы сразу, а накуролесила страшных делов в процессе работы. Поэтому пока мы не присвоили указателю какого-то конкретного адреса, мы его «занулили» для собственной безопасности.
Итак, прежде чем двигаться дальше подобьём итоги: указатель это 32-ух битная (или 64-ёх битная) переменная, которая хранит в себе не данные, а адрес какой-то одной ячейки памяти.
Типы
То же самое касается и других типов переменных. Например для типа float будет так…
Ниже мы ещё вернёмся к переменной digit и другим типам данных.
Присваивание адреса указателю и «взятие адреса» обычной переменной
Далее давайте присвоим нашему указателю конкретный адрес, на который он будет указывать.
Эта операция называется » взятие адреса «. Выше я писал — «Если мы захотим получить не содержимое ячейки, а её адрес, то для этого есть специальная конструкция», это оно и есть. Таким образом мы можем получить адрес любой переменной.
То есть наша программа будет выглядеть так…
Если добавим вот такой вывод на печать…
… то получим искомые данные…
Здесь, и ниже, на картинках, у меня 64-ёх битный указатель — не обращайте на это внимание. Просто я поленился рисовать восемь клеточек на схемах выше.
Вот тоже самое, только выполнено на микроконтроллере stm32.
Здесь указатель 32-ух битный.
Разыменования указателя
Теперь разберёмся с ещё одной важной вещью. Выше я писал что при работе с указателем звёздочка выступает в двух ипостасях, с первой мы познакомились, это объявление указателя, а вторая это получение данных из ячейки на которую указывает указатель, или запись данных в эту ячейку. Это называется «разыменование указателя».
По сути это действо обратно «взятию адреса» обычной переменной, только вместо амперсанда используется звёздочка, а вместо имени переменной, имя указателя.
Результат будет таков…
Разыменование указателя работает в обе стороны, то есть мы можем не только прочитать значение, но и записать в разыменованный указатель. То есть мы запишем новое значение в ячейку на которую указывает указатель.
Изменим наш пример…
Смотрим что получилось…
Можем в функциях printf() заменить разыменованный указатель (*ptr) на имя переменной…
Как вы уже наверно начинаете понимать, указатель это весьма любопытный инструмент, и мы уже начали использовать его по разному, но погодите, дальше будет интересней.
Термин «разыменованный указатель» вовсе не означает что указатель куда-то пропадает из-за того что мы «лишили его имени» и теперь он где-то бродит безымянный и неприкаянный, нет, просто это такой не самый удачный термин, а указатель как был так остаётся указателем со своим именем.
Если хотим изменить адрес на который указывает указатель, тогда просто присваиваем указателю новый адрес. Для примера поочерёдно присвоим одному и тому же указателю адреса разных переменных…
Видно что указатель указывает на три разных адреса трёх наших переменных. Заодно видим что переменные расположились в памяти друг за дружкой.
Ну и конечно можем для каждой переменной создать свой указатель…
Поскольку в использовании звёздочки прослеживается некое противоречие (сначала она означает объявленный указатель, потом разыменованный), стоит повторить всё что касается этого вопроса для закрепления информации.
Первое. Когда мы объявляем указатель, мы ставим звёздочку — здесь всё просто и понятно.
Второе. При использовании указателя по ходу программы. Когда мы используем имя указателя без звёздочки, мы получаем адрес ячейки на которую он указывает…
Разумеется в дальнейшем мы будем использовать указатель без звёздочки не только для вывода адреса на печать.
Когда мы ставим звёздочку перед именем указателя, мы получаем содержимое ячейки на которую он указывает…
Или делаем запись нового значения в ячейку на которую он указывает…
С этой звёздочкой у людей частенько возникают трудности из-за неправильного использования, так что будьте внимательны.
Здесь стоит заострить внимание читателя. Как я уже говорил выше, сам указатель либо 32-ух битный, либо 64-ёх битный (это для нас не имеет никакого значения), но вот тип данных на которые он указывает, может быть различный, и это очень важно. Поэтому когда мы при объявлении указателя прописываем тип, этот тип относится именно к типу данных на которые будет указывать указатель.
В памяти получилась следующая картина (рисунок я оставил прежний чтоб не перерисовывать)…
Теперь указатель хранит адрес первой ячейки 16-ти битной переменной (стрелочкой указывает на неё), а поскольку при объявлении указателя мы сообщили компилятору что указатель будет указывать на 16-ти битный тип, то программа знает что при обращении к указателю нужно прочитать ячейку на которую он указывает, и следующую за ней ячейку, то есть две ячейки — 5677 и 5678…
Таким образом, благодаря типу прописанному при объявлении указателя, программа знает какое количество ячеек нужно прочитать при обращении к этому указателю.
Программа будет знать что при обращении к этому указателю нужно прочитать только одну ячейку, на которую он указывает.
Программа будет знать что при обращении к этому указателю нужно прочитать ячейку, на которую он указывает, и следующую за ней.
Программа будет знать что при обращении к этому указателю нужно прочитать ячейку, на которую он указывает, и ещё три следующие за ней.
Программа будет знать что при обращении к этому указателю нужно прочитать ячейку, на которую он указывает, и ещё три следующие за ней (тип float занимает четыре байта).
Таким образом, тип указателя должен всегда точно соответствовать типу переменной на которую он будет указывать.
Теперь когда мы немного познакомились с указателем, давайте посмотрим как он работает на практике. Создадим простейшую программу…
Результат работы будет таков…
Мы передали переменную digit в функцию func_var(), увеличили на единичку и вывели на печать. Потом в основной функции тоже вывели на печать эту переменную, и разумеется получили результат без увеличения. Это произошло потому, что когда мы передавали переменную в функцию, мы её как-бы скопировали в другую переменную, объявленную в аргументе (uint16_t var). Переменная var в функции func_var() увеличилась, а оригинал как был так и остался равен 2300.
А сейчас изменим нашу программу вот так…
Результат получим иной…
В обоих случаях значение увеличилось на единицу.
В основной функции мы объявили указатель, присвоили ему адрес переменной digit, и передали этот указатель в функцию func_var(). Аргументом этой функции мы объявили указатель (ptr_var) в который при передаче записался адрес переменной digit. Это значит, что теперь указатель ptr_var так же как и указатель ptr_digit указывает на адрес переменной digit, и следовательно манипулируя указателем ptr_var мы можем изменить значение этой переменной.
Основную функцию мы можем немного упростить, сделав её такой…
Результат мы получим тот же, что и в предыдущем примере.
Здесь мы не стали объявлять указатель и присваивать ему адрес переменной, а просто воспользовались операцией «взятие адреса» и передали этот адрес в функцию.
Оба варианта идентичны по своему смыслу, однако я хотел показать, что можно передавать и указатель, и «голый» адрес.
Суть этих примеров с переменной digit заключалась в том, чтобы показать, что когда мы передаём переменную в какую-то функцию, то её изначальное значение не изменится, а если мы передаём указатель на эту переменную, то можем менять изначальное значение локальной переменной откуда угодно. Однако во всей красе возможности указателя раскрываются с другими типами данных, например с массивом.
Массив
Все программисты используют в своих программах массивы, но не все знают, что массив, а точнее имя массива это указатель указывающий на первый элемент этого массива. При этом объявляется он без всяких звёздочек. То есть в чём то он похож на «обычную» переменную. Если быть ещё более точным, то массив можно представить себе как набор однотипных переменных, расположенных в памяти друг за дружкой, а каждая из этих «переменных» является элементом массива.
Значение в квадратных скобочках говорит о том, сколько элементов содержится в этом массиве, а тип говорит о том, какого размера элементы этого массива, то есть сколько ячеек памяти занимает один элемент. Для примера возьмём такой массив…
Массив из четырёх элементов. Каждый элемент занимает в памяти одну ячейку (об этом говорит тип char). В каждый из элементов мы записали по одному символу, то есть инициализировали весь массив конкретными значениями.
Чтобы вывести массив на печать делаем так…
Здесь всё выглядит так, как будто мы обратились к «обычной» переменной и вывели её на печать. Тем не менее легко доказать что array всё таки указатель. Достаточно изменить форматирующий символ «s» на «p»…
И мы получим адрес…
Если же мы сделаем разыменование array …
То получим первый элемент массива…
Что доказывает сказанное выше — имя массива это указатель на первый элемент этого массива.
То же самое мы получим если добавим к имени индекс нулевого элемента массива…
В памяти это представляется следующим образом…
Имя array указывает на первый элемент массива (ячейка 5676), а следом идут остальные три элемента.
Чтобы нам было удобно обращаться к отдельным элементам этого массива компилятор любезно присвоил элементам индексы, начиная с нулевого. То есть ячейка 5676 получает индекс 0, ячейка 5677 получает индекс 1, ячейка 5678 получает индекс 2, и т.д. Важно помнить что отсчёт элементов ведётся от ноля.
На схеме индексов не видно, но программа знает какой ячейке присвоен какой индекс.
Квадратные скобки при использовании массива имеют двойное назначение. При объявлении массива в них указывается количество элементов, а в процессе работы индекс ячейки, то есть её порядковый номер в данном массиве.
Благодаря индексации мы легко и просто можем обращаться к любому элементу…
Чаще всего индексацию используют в циклах для записи в массив новых значений…
Переменная «i» приращивается в цикле и выступает в роли индекса элемента массива. Таким образом мы заполним все элементы символом «Z»…
Теперь создадим массив из двух элементов с типом uint16_t …
В таком массиве каждый элемент занимает две ячейки памяти…
Имя массива указывает на первую ячейку памяти первого элемента, а сами элементы хранят значения которые мы записали туда при инициализации массива.
Здесь индексы опять же присваиваются элементам массива. Индекс [0] отвечает за ячейки 5676 и 5677, а индекс [1] за ячейки 5678 и 5679. То есть индекс перескакивает через одну ячейку так как благодаря указанному типу uint16_t программа знает что каждый элемент массива занимает две ячейки памяти.
Чтоб проверить как работает индексация мы сначала прочитаем что храниться в элементах массива, а следом запишем в них число 999…
Получим что ожидали…
Как и в случае с «обычной» переменной, мы можем к элементу массива применить операцию «взятия адреса»…
А теперь давайте зафиксируем мысль на этом последнем примере и перейдём к следующему, очень важному понятию в теме про указатели, к «адресной арифметике» или «арифметики с указателями».
Адресная арифметика
Оперируя указателями мы оперируем хранящимися в указателях адресами, а адреса в свою очередь это всего лишь цифры, а раз это цифры, то значит мы можем производить над ними арифметические действия. То есть если вычесть или прибавить к указателю какую-то цифру, то этот указатель будет указывать уже на другую ячейку памяти. Вроде бы всё просто, но здесь есть существенный нюанс — вся эта арифметика жёстко связана с типом указателя. Сейчас мы убедимся в этом воспользовавшись нашим последним примером.
Освежим в голове нашу схему…
И добавим в последний пример ещё одну строчку…
А теперь смотрим что получилось на самом деле…
Как я уже говорил выше, адресная арифметика жёстко привязана к типу указателя, поэтому когда мы прибавили к указателю единицу он увеличивается не на 1, а на размер элемента массива. То есть наша конструкция выглядела как «плюс один элемент». Тип массива у нас uint16_t, значит размер элемента два байта, поэтому программа увеличила адрес на 2, и поэтому мы получили адрес первой ячейки второго элемента, а не то, что предполагали. А если бы мы применили эту конструкцию ко второму элементу, то ещё и вылетели бы за границы массива.
Этот нюанс нужно хорошенько запомнить, ибо многим начинающим программистам он стоил немалого количества вырванных волос и сломанных клавиатур .
Вот если мы будем работать с массивом типа char (или uint8_t), тогда адресная арифметика будет работать как обычная. Размер элемента один байт, значит и адрес будет увеличиваться на единицу.
Все адреса подряд.
Адресную арифметику удобно применять при парсинге строк. Например у нас есть массив со строкой (в языке СИ нету строк, есть только массивы), и нам нужно вывести на печать эту строку начиная с четвёртого символа, тогда делаем так…
Отрезали три первых символа.
Или допустим мы хотим перегнать из одного массива в другой строку начиная с четвертого символа…
Все действия я прокомментировал.
И вот вам ещё один пример демонстрирующий крутость указателя. В этой программе мы легко и непринуждённо уберём все нижние подчёркивания и запятые из строки…
Здесь я не буду ничего комментировать. В среде программистов бытует мнение, что указатель нельзя выучить, его можно только понять, как озарение. Сам через это проходил. Поэтому когда вы поймёте что происходит в этом примере, это будет означать что вы поняли указатель
Ну, а после понимания, всякие штуки типа указателя на указатель, и функции-указатели вы будете щёлкать как орешки.
Кстати, любопытная вещь — имя обычной функции, только без скобочек — это указатель на эту функцию, то есть адрес в памяти где расположена эта функция. Ради интереса можете добавить в последний пример строчку.
Это всё, всем спасибо