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 uch[10] = {0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC};

 

Данный массив также занял в памяти какое-то место и у каждого элемента массива теперь есть свои адреса в памяти

 

 

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

А как же получить указатель на массив, ну, то есть на то место с которого он начинается, на первый его элемент, вернее, если быть точным, на нулевой?

А, в принципе, его получать-то особо не всегда надо, так как имя массива и есть указатель на массив. Вот такая интересная получается история.

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

 

unsigned char *p_uch = &uch[0];

 

Мы в данном случае получаем адрес его нулевого элемента и присваиваем его переменной-указателю такого же типа, как и наш массив.

Теперь переменная p_uch хранит в себе адрес массива, то есть является указателем на массив uch

 

 

Здесь конечно же возникнуть может ещё один резонный вопрос. Почему указатель на наш массив занял в памяти 4 ячейки, а, следовательно, и 4 байта?

А потому, что указатель хоть и является указателем на тип unsigned char, но сама переменная-указатель всегда будет 32-битной, так как по-другому и не получится. Каким образом мы в 8 битах можем хранить число 0x0061FF11? Да никаким, вот поэтому и 4 ячейки.

Причём, наш указатель на массив, который мы только что создали, мы также можем использовать как имя нашего же массива. Если мы, например обратимся с помощью выражения p_uch[5], то мы получим имя 5 элемента массива uch. Это конечно уже тема не данного урока, а больше арифметика указателей, но тем не менее мы можем это делать.

 

В следующей части нашего урока мы отработаем все изученные в данной части урока приёмы с указателями и адресами на практике.

 

Предыдущий урок Программирование на C Следующая часть

 

 

Смотреть ВИДЕОУРОК (нажмите на картинку)

 

C Указатели и адреса

3 комментария на “C Урок 26. Указатели и адреса. Часть 1
  1. Артём:

    Здравствуйте!
    Судя по всему в последнем рисунке ошибка. В указателе на массив должен храниться адрес 0x0061FF11, а указан 0x0061FF12.

    • Простите, пожалуйста! Я очень спешил. Не думал, что это так критично.
      Дело в том, что делал таблички в Excel, и, когда там рисуешь стрелку, она выглядит в редакторе не совсем так, как потом в превью (со сдвигом) и в редакторе она показывала на 12-й адрес.
      Всё поправил.
      Спасибо за такую внимательность!

  2. serg_555:

    Спасибо большое очень доходчиво обьяснено !!!

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*