В предыдущей части нашего урока мы познакомились с указателями, адресами, изучили операцию разыменовывания, также узнали, как создать указатель на массив.
Ну, теперь, наконец-то, наступила практическая часть, хотя думаю, что после того, что мы увидели в теоретической части, нам не нужна никакая практика, там уже всё было. Но тем не менее, чтобы лучше прочувствовать тему указателей и адресов, считаю, что с кодом надо поработать.
Проект мы также создадим из проекта прошлого урока с именем MYPROG25 и дадим ему имя MYPROG26.
Откроем наш проект в Eclipse, произведём его первоначальную настройку и удалим весь наш код из функции main() за исключением возврата. Функция main() приобретёт вот такой вид
int main()
{
return 0; //Return an integer from a function
}
Объявим и проинициализируем переменную, а затем выведем в консоль её значение
1 2 3 4 5 |
int main() { unsigned int a; a = 0xFE340056; printf("Value a istt0x%08Xn", a); |
Объявим переменную-указатель такого же типа
1 2 |
printf("Value a istt0x%08Xn", a); unsigned int *p_a; |
Возьмём адрес у переменной a и присвоим его указателю
1 2 |
unsigned int *p_a; p_a = &a; |
Выведем в консоль значение указателя, это и будет адрес нашей переменной a
1 2 |
p_a = &a; printf("Value p_a istt0x%08Xn", (unsigned int)p_a); |
Запустим нашу программу, собрав проект и посмотрим значения самой переменной a, а также указателя на неё, который является и её адресом
Запустим отладку и узнаем, действительно ли это адрес нашей переменной a.
Для этого поставим вот тут точку останова и выполним код до неё
Мы видим, что переменные получили свои значения
И интересно то, что у переменной p_a есть теперь раскрывающая птичка слева, которая нам даёт понять, что данная переменная необычная. Откроем эту птичку
Мы видим там число, равное значению переменной a, что уже позволяет быть уверенным в том, что наш указатель — это указатель именно на эту переменную.
Но мы пойдём глубже. Откроем окно дизассемблирования. Вот здесь мы видим, что значение переменной a ушло в стек со смещением 0x18
Посмотрим адрес стека
Получается, что наша переменная a лежит в ячейке памяти с адресом 0x61FF18. Откроем его в окне Memory
Ничего вам это число не понимает? Так это же наша переменная a! Правда байты следуют в обратном порядке. Ну так оно и должно быть. Байты следуют младшим вперёд.
Мы теперь знаем адрес нашей переменной a без всяких указателей. Но мы помним, как мы смотрели значение указателя в окне переменных. Оно у нас такое и было!
Далее мы берём значение адреса переменной a и укладываем его по другому адресу в стек со смещением 0x1С
Вот там и будет лежать наш указатель. То есть у нас такая ситуация, что указатель с адресом лежит рядом с самим адресом, на который он указывает, в ячейке с адресом 0x61FF1С.
Посмотрим это в памяти
Вот он, рядышком, и тоже перевёрнутый.
Так что, отладка — великая сила!
Я пока не планирую давать уроки по отладке, но если вы смотрите все мои уроки, то научитесь пользоваться и отладкой. Потихоньку мы и её постигаем.
Вернёмся в наш проект и попробуем теперь разыменовать наш указатель, чтобы получить из него значение, хранящееся по адресу, который он хранит ну или на который он указывает.
А сделаем мы это, не заводя никаких лишних переменных, просто разыменуем указатель сразу в аргументе функции printf и выведем его значение в консоль
1 2 |
printf("Value p_a istt0x%08Xn", (unsigned int)p_a); printf("Value *p_a istt0x%08Xn", *p_a); |
Посмотрим результат нашего разыменования
Всё отлично!
За счёт операции разыменования мы получили обратно значение нашей переменной a, то есть мы узнали у первого человека возраст второго человека, адрес которого он знает.
По идее, с отладкой всё понятно, но здесь я не удержался и всё-таки покажу вам операцию разыменования в дизассемблере
Процессор здесь сначала берёт значение адреса, которое у нас хранится в стеке, а затем по адресу, хранящемуся в регистре eax забирает значение, хранящееся по этому адресу, и записывает его в тот же eax. Вот это экономия! Даже не подумаешь, что так можно делать. По идее, как вообще выполняется такая инструкция, в голове не умещается. Это же одна элементарная ячейка памяти в самом процессоре. А инструкция такая существует и имеет свой машинный опкод. Можно его посмотреть. Вызовем контекстное меню в окне дизассемблера и заставим его показывать опкоды
Теперь у нас видны машинные коды инструкций. И вот он — код операции взятия значения по адресу, хранящемуся в регистре eax с записью обратно в него же
Вот и всё разыменование.
Ну ладно, делу время потехе час.
Продолжим.
Попробуем теперь обратную операцию. Мы присвоим разыменованному указателю другое число и посмотрим, изменится ли от этого значение переменной a
1 2 3 4 |
printf("Value *p_a istt0x%08Xn", *p_a); printf("-------------------------------------n"); *p_a = 0xE2222222; printf("Value a istt0x%08Xn", a); |
Запустим наш код и посмотрим результат
Таким образом, засчёт операции присвоения значения разыменованному указателю мы изменили значение переменной, никак не используя её имя.
Ну, а с отладкой, думаю, поиграетесь сами. Надеюсь, вы поняли, как это интересно и к тому же очень полезно.
Поработаем ещё с нашим указателем и назначим его теперь указателем адреса другой переменной, тем самым покажем то, что указатель не привязывается ни к каким переменным и может в разное время хранить адреса разных переменных.
Добавим ещё одну переменную и покажем её значение в консоли
1 2 3 4 |
printf("Value a istt0x%08Xn", a); printf("-------------------------------------n"); unsigned int b = 0xFAFAFAFA; printf("Value b istt0x%08Xn", b); |
Присвоим теперь её адрес нашему указателю
1 2 |
printf("Value b istt0x%08Xn", b); p_a = &b; |
Покажем в консоли значение указателя, а также его же значение, но разыменованное, что по определению является значением переменной b
1 2 3 |
p_a = &b; printf("Value p_a istt0x%08Xn", (unsigned int)p_a); printf("Value *p_a istt0x%08Xn", *p_a); |
Запустим наш код
Всё работает. Указатель наш теперь показывает на место в памяти, где хранится переменная b.
Поработаем с массивом.
Объявим и проинициализируем массив
1 2 3 4 |
printf("Value *p_a istt0x%08Xn", *p_a); printf("-------------------------------------n"); unsigned char uch[10] = {0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC}; |
Зная то, что имя массива — это его указатель в памяти, покажем значение этого указателя в консоли
1 2 |
0x88, 0x99, 0xAA, 0xBB, 0xCC}; printf("Value uch istt0x%08Xn", (unsigned int)uch); |
Соберём наш код и посмотрим значение этого указателя, который и является адресом начала массива в памяти, то есть того места, где хранится его нулевой элемент
Проверим это в отладке
Мы видим здесь и адрес в памяти и значения всех элементов массива.
Ну и, никак нельзя без дизассемблера. После него уже материал ложится в голову навечно
Найдём теперь наш массив в памяти. У нас немного изменился адрес стека
Найдём данную область памяти и посмотрим значения в ней
Каким образом изменить формат показа значений с четырёхбайтного на однобайтовый, я показал в видеоверсии урока.
Итак, мы нашли наш массив в памяти.
Попробуем присвоить адрес массива другому указателю. Сделать это можно примерно вот так
1 2 |
printf("Value uch istt0x%08Xn", (unsigned int)uch); unsigned char *p_uch = &uch[0]; |
Мы взяли адрес у нулевого элемента массива — это и есть адрес массива в памяти и создали на него указатель p_uch.
Теперь попробуем воспользоваться данным указателем, как именем массива и попробуем вывести элементы массива, пользуясь именем указателя на него
1 2 3 |
unsigned char *p_uch = &uch[0]; printf("Value p_uch istt"); for(int i=0; i<10; i++) printf("0x%02X ", p_uch[i]); |
Посмотрим результат наших операций
Всё отлично! Мы можем пользоваться именем указателя на массив как и самим массивом, ну и наоборот тоже. Это подтвердил предыдущий код.
Теперь указатель на указатель. Создадим такой указатель
1 2 3 |
for(int i=0; i<10; i++) printf("0x%02X ", p_uch[i]); printf("n-------------------------------------n"); unsigned int **p_p_a; |
Как мы помним, мы используем в данном случае две звёздочки. Также может быть и указатель на указатель на указатель и так далее. Но это редко используется. Ну уж про пять звёзд я не слышал точно, но такое вполне возможно.
Присвоим указателю p_a адрес переменной a, а то, помнится, он хранит у нас адрес переменной b
1 2 |
unsigned int **p_p_a; p_a = &a; |
А указателю p_p_a присвоим адрес указателя p_a
1 2 |
p_a = &a; p_p_a = &p_a; |
Выведем значения в консоль. Это будет наш указатель на указатель, затем он же но разыменованный и он же, но дважды разыменованный
1 2 3 4 |
p_p_a = &p_a; printf("Value p_p_a istt0x%08Xn", (unsigned int)p_p_a); printf("Value *p_p_a istt0x%08Xn", (unsigned int)*p_p_a); printf("Value **p_p_a ist0x%08Xn", (unsigned int)**p_p_a); |
Посмотрим, что из этого получилось
А получилось всё отлично!
Мы добрались через указатель на указатель до значения самой переменной a. Применив двойное разыменование, мы всё-таки получили возраст третьего человека, адрес которого знает второй человек, адрес которого знает первый человек у этого самого первого человека. Мы даже не будем ничего смотреть в отладке, но воообще было бы интересно, жаль что время не резиновое. Но вам я советую всё же посмотреть.
Теперь попробуем изменить значение переменной a через указатель на указатель на неё и выведем после этого в консоль значение переменной a
1 2 3 |
printf("Value **p_p_a ist0x%08Xn", (unsigned int)**p_p_a); **p_p_a = 0xBEBEBEBE; printf("Value a istt0x%08Xn", a); |
Посмотрим результат
Всё отлично! Мы изменили значение переменной a, достучавшись до неё по иерархии через два адреса.
Таким образом, сегодня мы узнали, что такое указатели и что такое адреса, поняли, как их можно использовать, а также познакомились с операцией разыменования указателя. Но также мы должны понимать, что по указателям это ещё не всё, это только первое знакомство, будет ещё и адресная арифметика, и передача указателей на данные в аргументах функции, будут указатели на структуры, указатели на сами функции.
Так что вас ждёт ещё много интересного!
Всем спасибо за внимание!
Предыдущая часть Программирование на C Следующий урок
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Добавить комментарий