как присвоить адрес указателю
Указатели в C++: адрес и удаление
Всем привет! В этом уроке мы разберём то, что очень тесно связано с памятью в компьютере. С помощью этого можно улучшать работу своей программы. Как вы догадались с названия урока, это — указатели.
Адрес переменной в C++
Давайте приведем аналогию с круизным лайнером. В нем, как и в отеле, имеются номера. Вот, например, при покупке номера вам могут дать номер — 0x155 (да, мы понимаем, что не в одном лайнере или отеле не станут записывать номера в шестнадцатеричном виде, но давайте все таки немного отвлечемся). А друг может оказаться в номере 0x212 — так и с переменными, они могут получить разный путь. И только сам компьютер при создании переменной знает, где она находится.
Переменные, которые вы создаете в программе, по её завершению автоматически удаляются, чтобы не нагружать операционную память вашего компьютера.
Пример удаления переменных
В играх присутствует хорошая графика, различные спецэффекты. Например, тот же дым. Все это — переменная (может не одна!), которой в будущем придётся уничтожиться навсегда. А вот, если бы она не удалилась, то она бы своей фоновой работой понемножку нагружала бы наш компьютер.
Поэтому в C/C++ присутствует возможность обратиться к переменной, и, если требует ситуация, удалить и создать её вовсе в другом участке программы, когда это, конечно, нам будет нужно.
Что такое указатели в C++
Указатели — это с самого начала переменные, уже в которых хранится адрес других переменных.
Чтобы пользоваться указателями, вам нужно использовать два оператора:
Как создать указатели в C++
Давайте посмотрим, какую конструкцию нужно использовать, чтобы создать указатели:
C Урок 26. Указатели и адреса. Часть 2
В предыдущей части нашего урока мы познакомились с указателями, адресами, изучили операцию разыменовывания, также узнали, как создать указатель на массив.
Ну, теперь, наконец-то, наступила практическая часть, хотя думаю, что после того, что мы увидели в теоретической части, нам не нужна никакая практика, там уже всё было. Но тем не менее, чтобы лучше прочувствовать тему указателей и адресов, считаю, что с кодом надо поработать.
Проект мы также создадим из проекта прошлого урока с именем MYPROG25 и дадим ему имя MYPROG26.
Откроем наш проект в Eclipse, произведём его первоначальную настройку и удалим весь наш код из функции main() за исключением возврата. Функция main() приобретёт вот такой вид
int main()
return 0; //Return an integer from a function
Объявим и проинициализируем переменную, а затем выведем в консоль её значение
Объявим переменную-указатель такого же типа
Возьмём адрес у переменной a и присвоим его указателю
Выведем в консоль значение указателя, это и будет адрес нашей переменной a
Запустим нашу программу, собрав проект и посмотрим значения самой переменной a, а также указателя на неё, который является и её адресом
Запустим отладку и узнаем, действительно ли это адрес нашей переменной a.
Для этого поставим вот тут точку останова и выполним код до неё
Мы видим, что переменные получили свои значения
И интересно то, что у переменной p_a есть теперь раскрывающая птичка слева, которая нам даёт понять, что данная переменная необычная. Откроем эту птичку
Мы видим там число, равное значению переменной a, что уже позволяет быть уверенным в том, что наш указатель – это указатель именно на эту переменную.
Но мы пойдём глубже. Откроем окно дизассемблирования. Вот здесь мы видим, что значение переменной a ушло в стек со смещением 0x18
Посмотрим адрес стека
Получается, что наша переменная a лежит в ячейке памяти с адресом 0x61FF18. Откроем его в окне Memory
Ничего вам это число не понимает? Так это же наша переменная a! Правда байты следуют в обратном порядке. Ну так оно и должно быть. Байты следуют младшим вперёд.
Мы теперь знаем адрес нашей переменной a без всяких указателей. Но мы помним, как мы смотрели значение указателя в окне переменных. Оно у нас такое и было!
Далее мы берём значение адреса переменной a и укладываем его по другому адресу в стек со смещением 0x1С
Вот там и будет лежать наш указатель. То есть у нас такая ситуация, что указатель с адресом лежит рядом с самим адресом, на который он указывает, в ячейке с адресом 0x61FF1С.
Посмотрим это в памяти
Вот он, рядышком, и тоже перевёрнутый.
Так что, отладка – великая сила!
Я пока не планирую давать уроки по отладке, но если вы смотрите все мои уроки, то научитесь пользоваться и отладкой. Потихоньку мы и её постигаем.
Вернёмся в наш проект и попробуем теперь разыменовать наш указатель, чтобы получить из него значение, хранящееся по адресу, который он хранит ну или на который он указывает.
А сделаем мы это, не заводя никаких лишних переменных, просто разыменуем указатель сразу в аргументе функции printf и выведем его значение в консоль
Посмотрим результат нашего разыменования
За счёт операции разыменования мы получили обратно значение нашей переменной a, то есть мы узнали у первого человека возраст второго человека, адрес которого он знает.
По идее, с отладкой всё понятно, но здесь я не удержался и всё-таки покажу вам операцию разыменования в дизассемблере
Процессор здесь сначала берёт значение адреса, которое у нас хранится в стеке, а затем по адресу, хранящемуся в регистре eax забирает значение, хранящееся по этому адресу, и записывает его в тот же eax. Вот это экономия! Даже не подумаешь, что так можно делать. По идее, как вообще выполняется такая инструкция, в голове не умещается. Это же одна элементарная ячейка памяти в самом процессоре. А инструкция такая существует и имеет свой машинный опкод. Можно его посмотреть. Вызовем контекстное меню в окне дизассемблера и заставим его показывать опкоды
Теперь у нас видны машинные коды инструкций. И вот он – код операции взятия значения по адресу, хранящемуся в регистре eax с записью обратно в него же
Вот и всё разыменование.
Ну ладно, делу время потехе час.
Попробуем теперь обратную операцию. Мы присвоим разыменованному указателю другое число и посмотрим, изменится ли от этого значение переменной a
Указатель в языке Си
Указатель — переменная, содержащая адрес объекта. Указатель не несет информации о содержимом объекта, а содержит сведения о том, где размещен объект.
Указатели широко используются в программировании на языке Си.
Указатели часто используются при работе с массивами.
Память компьютера можно представить в виде последовательности пронумерованных однобайтовых ячеек, с которыми можно работать по отдельности или блоками.
Каждая переменная в памяти имеет свой адрес — номер первой ячейки, где она расположена, а также свое значение. Указатель — это тоже переменная, которая размещается в памяти. Она тоже имеет адрес, а ее значение является адресом некоторой другой переменной. Переменная, объявленная как указатель, занимает 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;
>
C Урок 26. Указатели и адреса. Часть 1
На данном уроке мы рассмотрим интересную тему. Это указатели и адреса.
На данной теме очень много начинающих программистов впадают в ступор, особенно когда видят звёздочки и амерсанды, которые для этого используются в языке C.
Вернее, мы, конечно же, данную тему не рассмотрим. Мы её только легонько коснёмся. Рассмотреть в рамках одного урока такую серьёзную тему не получится. По ней будет как минимум 4 урока. Мы лишь проведём сегодня первое знакомство. Но это будет самое главное. Так как понимание указателей и адресов начинается именно здесь.
Надеюсь, если вы попали сюда, то вы решили освоить программирование на языке, что говорит о том, что с информатикой вы уже знакомы и преподавать её здесь я, конечно же, не буду ибо это огромная наука, что вы уже знаете, как организована архитектура компьютера, что такое память физическая, виртуальная, адресная шина, магистраль, процессор и т.д.
Напомню то, что память, предоставляемая программе или процессу операционной системой, имеет адресацию совершенно не ту, какую этот предоставленный участок занимает физически. То есть адреса совершенно другие и не факт, что кажущийся непрерывным участок памяти в виртуальном адресном пространстве будет также непрерывен в физическом. Он может быть фрагментирован. Но это нам лишь облегчает работу, так как нам не нужно озадачиваться тем, каким образом организована память физически. Поэтому, если я буду рассказывать здесь о памяти, то буду иметь в виду именно виртуальную, имеющую те адреса, которые мы видим в среде программирования в отладке.
Также, думаю, практически все в курсе, что у меня есть некоторое количество уроков по приобретающим всё большую популярность микроконтроллерам. Вот там как раз мы работаем напрямую с памятью физической и в отладке видим именно её. Но это не важно. Урок данный будет вполне актуален и для МК. Нам же абсолютно нет дела до того, какую память мы видим. А видим мы именно ту память, с которой мы работаем.
Организована память, предоставленная программе, примерно вот таким образом
У каждой ячейки памяти существует адрес.
Почему адреса следуют друг за другом не подряд, а пропускаются по 4 байта?
Можно конечно показать и подряд, но я показал именно с учётом того, что мы пишем приложение под 32-разрядную систему. Хотя у нас практически у большинства установлены операционные системы 64-разрядные, но приложения, написанные под 32-битные системы, там прекрасно работают. У нас даже компилятор mingw предназначен для 32-разрядных систем. Пока мы будем писать с расчётом именно на 32-разрядные системы, так как, во-первых, они легче для понимания, во-вторых, из соображений совместимости, так как 32-рязрядные системы пока ещё существуют и хочется, чтобы наша программа запускалась и прекрасно работала и на них. Также у нас получается то, что наш урок актуален и для 32-разрядных МК, например для тех же stm32.
Так вот к чему я это всё?
А к тому, что считаю, что удобнее работать также с 32-битными ячейками памяти и зачастую к ним идёт приравнивание.
Вообще в одной такой ячейке получается по 4 ячейки 8-битных. Например, если мы объявляем переменную типа char и присваиваем ей какое-то значение, то это значение займёт только такую ячейку.
Ну что ж. Представим, что мы объявили переменную типа int, которая скорей всего занимает в памяти 32 бита (но не факт, это иногда проверять надо), назвав её, например a. Затем присвоили ей какое-то значение. В данном случае нам операционная система выделит какую-то ячейку памяти, ну пусть, например вот эту
Теперь у нашей переменной появился адрес, так как операционная система предоставила ей определённую ячейку памяти. Операционная система не знает имя этой переменной, оно ей не нужно, даже и процессор при выполнении программы не знает имени переменной, он работает с её значением и обращается к ней именно по адресу. А как же мы можем узнать адрес этой переменной?
Мы конечно же зададимся вопросом. А зачем нам адрес переменной? Мы же знаем её имя и в процессе написания программы мы прекрасно можем обратиться к нашей переменной по имени. Скажу лишь, что есть такие ситуации, когда нам требуется именно адрес переменной, так как, например, если мы передали значение переменной в другую функцию в качестве параметра, то у нас в данной функции создастся копия этой переменной и мы будем работать с ней, поэтому, если мы вдруг решим изменить значение нашей переменной, то мы не сможем этого сделать, имя её в другой функции не видно, то есть переменная не попала в область видимости. Но если мы как-то передадим адрес, то мы сможем уже работать с реальной переменной. И это лишь одна ситуация, таких очень много.
Чтобы нам как-то запомнить адрес нашей переменной, то есть тот, который в нашем случае 0x0061FF14, то мы можем создать для нашей переменной указатель.
Для этого мы можем заранее объявить переменную-указатель типа int. Указатель такого типа будет указывать на переменные именно такого типа.
Объявляется переменная-указатель какого-либо типа данных вот таким образом
Объявление указателя очень похоже на объявление обычной переменной, только тут появляется наша пресловутая звёздочка, которая почему-то всех путает. А почему именно, да потому что ставится она возле имени переменной, а после типа данных ставится пробел. Можно, конечно, поступить и наоборот, поставить эту звёздочку возле типа данных, потом поставить пробел и лишь потом имя. Всё работать будет точно так же и это было бы правильнее для понимания, так как звёздочка относится именно к типу данных. Именно тип данных вместе со звёздочкой и есть тип переменной-указателя. Но существует соглашение, согласно которому звёздочка ставится перед именем. Поэтому мы просто должны чётко понимать, что тип данных вместе со звёздочкой – это то, что мы объявили переменную-указатель, а её имя – это только имя указателя. Потом ещё будет причина непонимания смысла указателей, когда мы встретим звёздочку немного в другом случае. Но это чуть позже.
Как это всё будет выглядеть практически?
Сначала давайте объявим нашу обычную переменную a и присвоим ей какое-нибудь значение. Пусть она будет даже беззнаковая
unsigned int a;
a = 0xFE340056;
А теперь мы объявим переменную-указатель такого же типа
unsigned int *p_a;
Имя нашей переменной p_a. Я специально дал такое имя, чтобы показать то, что указатель будет у нас хранить адрес переменной a. Пока мы этот указатель только объявили, мы ему не присваивали никаких адресов. Это обычная переменная, которой также выделена ячейка памяти, но она пока не хранит никакой информации, пока там какое-то случайное значение.
Вообщем, в памяти у нас пока примерно вот такая картина
То есть, у нас есть переменная типа указателя на unsigned int, которая пока не хранит практически ничего, а переменная a имеет уже значение, хранящееся по определённому адресу.
Как же присвоить адрес нашей переменной a нашему указателю p_a?
А делается это вот таким вот образом.
Существует специальная операция, называемая операцией взятия адреса.
Оператор данной операции выглядит в виде амперсанда
Данная операция является унарной, используется в выражениях и оператор амперсанд ставится перед переменной, адрес которой мы хотим получить
Здесь тоже присутствует некая путаница. Кто-то путает данный оператор с точно таким же оператором побитового И, ровно как звёздочку изредка расценивают как операцию умножения.
Давайте теперь присвоим адрес нашей переменной a переменной-указателю p_a
p_a = &a;
Здесь мы больше никаких звёздочек не используем. Звёздочка была нужна только при объявлении переменной-указателя. Теперь мы с ней работаем, как с обычной переменной и присваиваем ей адрес другой переменной, используя операцию взятия адреса.
Вот теперь переменная p_a хранит в себе адрес переменной a
Получается, что p_a знает теперь адрес a. То есть если, как в жизни, у нас есть такой человек, который знает адрес другого человека.
И теперь у нас наступила такая ситуация. Если нам потребовался адрес другого человека, то как мы его можем узнать у того человека, который этот адрес знает.
Ну конечно же любой ответит: Да как! Спросить у него надо!
Ну это в жизни. А здесь как? Да вообще элементарно. У нас p_a его и хранит, он является его значением. Просто присваиваем значение этого p_a любым переменным.
Тогда будет другой вопрос. А можем ли мы узнать у p_a не адрес, а значение нашей переменной a, адрес которой он хранит? Ну это типа спросить у человека, знающего адрес человека: А не знаешь ли ты, сколько лет другому человеку?
Ответ: мы это также можем сделать. Для этого мы должны произвести операцию разыменования указателя p_a.
Для разыменования у нас существует вот такой оператор
Да автор просто над нами прикалывается! – скажете вы.
Нет, не прикалывается. Опять эта звёздочка!
Но здесь она уже служит унарным оператором разыменования указателя, который используется вот таким образом
Используется также в выражениях, но не только. Использование такой конструкции возможно перед оператором присваивания. Но об этом чуть позже.
Как же отличить звёздочку, обозначающую указатель, от звёздочки, являющейся оператором разыменования указателя? А очень просто. Во втором случае перед звёздочкой не будет типа данных.
Поэтому, чтобы получить значение нашей переменной a, используя её указатель, мы можем сделать, например вот так
unsigned int b = *p_a;
Теперь переменная b получит значение переменной a, которое мы взяли у указателя на эту переменную, применив операцию разыменования.
С помощью разыменования мы можем также и изменить значение переменной a, используя указатель p_a, который хранит её адрес
*p_a = 0xE2222222;
Может также возникнет вопрос. Если указатель – это также переменная, которая тоже имеет в памяти свой адрес. Она же находится в памяти и занимает ячейку. А можем ли мы создать и на неё указатель?
Ну конечно же можем!
Указатель на указатель мы можем объявить с помощью двух звёздочек, вот таким вот образом
unsigned int **p_p_a;
Наш новый указатель теперь тоже получит своё место в памяти
Только этот указатель пока ни на что не показывает, то есть он пока не хранит никаких адресов.
И теперь мы вот таким образом присваиваем адрес указателя p_a, который указывает на переменную a, нашему новому указателю
p_p_a = &p_a;
Получится теперь у нас вот такая картина в памяти
То есть теперь p_p_a хранит адрес p_a, который в свою очередь хранит адрес a.
Вообщем какой-то человек знает адрес другого человека, который в свою очередь знает адрес третьего человека.
Ну и тогда резонный вопрос: а можем ли мы у этого человека узнать возраст третьего человека, ведь он знает адрес другого человека, который знает адрес третьего человека и который может спросить у третьего человека его возраст и сказать первому человеку?
Ну конечно можем. И причём в нашем случае программирования на языке C всё гораздо проще.
Мы также используем две звёздочки в операции разыменования
unsigned int b = **p_p_a;
У нас получилось как бы двойное разыменование. Мы разыменовали адрес указателя p_a, получив его значение, являющееся адресом a и второй звёздочкой разыменовали и этот адрес, получив значение уже самой переменной a.
Вот так. Теперь, надеюсь, к вам немного пришло понимание физики адресов. У адресов существует также ещё и арифметика, но об этом в другом уроке.
Теперь давайте немного поговорим о массивах.
Например существует у нас массив данных типа unsigned char, мы его объявили и сразу проинициализировали
Данный массив также занял в памяти какое-то место и у каждого элемента массива теперь есть свои адреса в памяти
Теперь нам удобнее смотреть в памяти все адреса, а не каждый четвёртый, так как у нас массив такого типа. Причём мы можем получить указатель на каждый его элемент, но нам это не нужно пока.
А как же получить указатель на массив, ну, то есть на то место с которого он начинается, на первый его элемент, вернее, если быть точным, на нулевой?
А, в принципе, его получать-то особо не всегда надо, так как имя массива и есть указатель на массив. Вот такая интересная получается история.
Но если нам вдруг нужно назначить указатель на массив другой переменной, то мы этот также можем прекрасно сделать, например, вот так.
unsigned char *p_uch = &uch[0];
Мы в данном случае получаем адрес его нулевого элемента и присваиваем его переменной-указателю такого же типа, как и наш массив.
Теперь переменная p_uch хранит в себе адрес массива, то есть является указателем на массив uch
Здесь конечно же возникнуть может ещё один резонный вопрос. Почему указатель на наш массив занял в памяти 4 ячейки, а, следовательно, и 4 байта?
А потому, что указатель хоть и является указателем на тип unsigned char, но сама переменная-указатель всегда будет 32-битной, так как по-другому и не получится. Каким образом мы в 8 битах можем хранить число 0x0061FF11? Да никаким, вот поэтому и 4 ячейки.
Причём, наш указатель на массив, который мы только что создали, мы также можем использовать как имя нашего же массива. Если мы, например обратимся с помощью выражения p_uch[5], то мы получим имя 5 элемента массива uch. Это конечно уже тема не данного урока, а больше арифметика указателей, но тем не менее мы можем это делать.
В следующей части нашего урока мы отработаем все изученные в данной части урока приёмы с указателями и адресами на практике.
Смотреть ВИДЕОУРОК (нажмите на картинку)