#26. Указатели. Проще простого | Язык C для начинающих

#26. Указатели. Проще простого | Язык C для начинающих18:48

Информация о загрузке и деталях видео #26. Указатели. Проще простого | Язык C для начинающих

Автор:

selfedu

Дата публикации:

24.04.2023

Просмотров:

55.2K

Транскрибация видео

Здравствуйте, дорогие друзья!

Я Сергей Балакирев, и мы продолжаем курс по языку C. Прежде чем двигаться дальше, изучать массивы, строки, функции и тому подобное, нужно вначале познакомиться с самым прекрасным, сакраментальным и основополагающим знанием – концепцией указателей.

Это невероятно простая тема, вводящая многих в трепетный ужас.

С чем это связано?

Не знаю, для меня это загадка.

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

А начнём, как всегда, с самого начала.

Из предыдущих серий вы уже знаете, что вычислительная техника снабжена оперативной памятью, которую можно представить в виде непрерывной последовательности ячеек, размером в 1 байт.

Каждая ячейка имеет свой уникальный номер, который называется адресом.

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

Поэтому предположим, что в нашей программе объявлена переменная типа char, например charD.

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

Если затем в программе объявляется еще одна переменная, ну допустим intf, то также автоматически где-то в памяти будет выделена свободная область для хранения этой переменной, причем под нее будет отведено уже 4 подряд идущих байт.

но если мы полагаем что размер типа int 4 байта при этом адресом переменной считается вот этот номер первой ячейки несмотря на то что это же переменная размещена еще и в ячейках 102 103 104 тем не менее адрес переменной f это

Это ячейка с номером 101.

Также обратите внимание, что ячейки для переменных f и d не пересекаются.

У каждой переменной строго своя область.

И это наиболее частый вариант, если мы сами намеренно не укажем делать иначе.

Именно так будем полагать далее.

То есть, смысл объявления переменных с точки зрения вычислительной техники – это размещение их в памяти устройства.

Под каждую переменную где-то в памяти отводится своя независимая область, и в этой области хранится значение соответствующей переменной.

Например, если переменной d присвоить значение 10,

то в ячейке, отведенной под эту переменную, будет занесено это значение 10.

А если переменной f присвоить, например, вот такое значение 75432, то в ее ячейках будут записаны вот такие числа 168, 38, 1 и 0.

Почему именно такие?

Потому что вот это число 75432 можно представить на уровне байтов вот таким вот выражением.

Это 168 плюс 256 умножено на 38, то есть это вот второе число интерпретируется именно так.

Ну и наконец 256 в квадрате умножено на единицу.

Если все просуммировать, то как раз получим 75432.

То есть так кодируется целое число на уровне байт.

И аналогичная картина будет для любых переменных языка C. Например, при объявлении вещественной переменной типа double в память будет отведено уже 8 байт.

И то, что содержится в этих 8 байтах и будет определять содержимое этой переменной.

Так хранятся любые переменные, которые мы объявляем в программах.

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

А раз это так, то изменение значений вот в этих ячейках автоматически приведет к изменению значений соответствующих переменных.

Но как мы можем изменить эти ячейки, не используя саму переменную?

Вот как раз это способны делать указатели.

Именно для этого они и существуют.

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

Это еще называется прямым доступом к памяти.

Так что же из себя представляют указатели?

Возможно, вас это удивит, но по сути это те же самые переменные, причем целочисленные.

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

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

Если система 32-разрядная, то указатели занимают в памяти устройства 4 байта, потому что 4 байт как раз будет достаточно для адресации к любой ячейке в 32-разрядных системах.

А если система 64-разрядная, то уже 8 байт.

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

Давайте теперь посмотрим, как в языке C можно объявить такой указатель Общий синтаксис здесь следующий Сначала прописывается тип данных, с которым будет работать указатель Затем ставится звездочка и следом идет имя указателя Обратите внимание, сам по себе указатель всегда хранит лишь адрес переменной И имеет фиксированный размер 4 байта для 32-разрядных систем и 8 байт для 64-разрядных

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

Сейчас на конкретных примерах вы все это увидите и поймете.

Давайте предположим, что далее в программе мы хотим работать с такой байтовой переменной, переменной d, которая имеет начальное значение 10.

И мы хотим с этой переменной взаимодействовать через указатель.

Так как переменная D имеет тип char, то при объявлении указателя также нужно прописать этот тип, тип char.

В результате получается такое-то объявление указателя char gpt.

Ну и в памяти соответственно отводится под него либо 8 байта из-за 64-битной системы, либо 4 байта, ну либо какое-то другое.

Будем полагать, что у нас 64-битная система и 8 байт.

Далее, для того, чтобы вот с помощью этого указателя работать с переменной d, этот указатель должен содержать адрес этой переменной.

В нашем случае адрес этой переменной равен 34 024.

Это просто как пример.

На самом деле эта переменная может располагаться в любой другой ячейке.

Но предположим, что именно в этой.

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

И в результате вот этот указатель GPT будет хранить это число следующим образом.

Значит, то здесь в первой ячейке будет 232, во второй 132, и вот таким вот образом будет как раз кодироваться значение 34 024.

Вот оно здесь расписано.

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

После этого мы можем совершенно спокойно посредством указателя GPT записывать значение в ячейку с номером 34 024, ну или считывать оттуда данные.

Делается это следующим образом.

Если мы прописываем вот такую вот конструкцию, то происходит считывание значений из ячейки памяти.

А если мы прописываем вот такую вот конструкцию, то, соответственно, заносится какое-то значение в эту ячейку памяти.

И обратите внимание, как здесь прописан указатель Перед его именем ставится вот такая операция звездочка То есть когда ставится звездочка и далее прописывается имя указателя То все вот это вместе, звездочка G5, мы можем воспринимать как переменную Которая расположена вот по этому адресу То есть по сути дела в нашем конкретном примере звездочка G5 и вот это имя D Это одно и то же

И то, и то как бы является именем вот этой вот ячейки, с которой мы можем работать.

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

И вот эта вот звездочка, которая ставится перед именем указателя g5,

после того, как мы его объявили, называется операция разымёнования.

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

В данном случае число 10.

А когда мы выполняем вот такую вот операцию, то наоборот, вот в эту ячейку с номером 34 024 будет занесено новое значение 100.

И здесь соответственно вот эта вот соточка появится.

По сути вот так вот в EZTC происходит объявление и использование указателей.

Конечно, сейчас у вас может возникнуть вопрос, зачем все это надо?

Есть же у нас переменная D, и с этой переменной напрямую можно работать с этой ячейкой памяти.

Какой смысл в этих указателях?

Но все по порядку.

И первый правильный вопрос такой.

Как нам в реальной программе узнать адрес расположения той или иной переменной?

Вот это число, как я говорил, 34 024, что мы использовали, это всего лишь иллюстрация.

В реальности эта же переменная D может находиться в любой доступной ячейке памяти.

Как же нам в программе узнать номер этой ячейки?

И здесь тоже все очень просто.

В EZTC есть специальный оператор, который возвращает адрес переменной.

Он определяется символом ampersand и записывается перед именем переменной.

Например, вот так вот.

Ampersand D.

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

И вот это универсальная конструкция.

Обратите внимание, мы прописываем вот в данном случае, когда присваиваем указателю какой-либо адрес, то указатель прописывается без звездочки.

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

И вот этот момент нужно себе очень-очень хорошо запомнить и знать.

Поэтому давайте еще раз его уточним.

Смотрите, предположим, мы в двух таких видах используем указатель.

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

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

Но в данном случае это вот эта первая ячейка с номером 0.

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

адрес которой хранится в этом указателе.

И в данном случае звездочка g5 присвоить 0 будет означать, что мы вот в эту ячейку с номером 0 пытаемся занести значение, равное 0.

То есть вот эта вот вторая строчка означает именно это.

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

И по сути дела является как бы именем,

вот этой вот ячейки.

То есть когда у нас в указатель GPT хранится именно вот этот вот адрес 0, то звездочкой GPT будет являться как бы именем вот этой вот ячейки.

И мы через это имя, так же как и через переменную вот,

Можем к этой ячейке обращаться, туда что-то записывать и, соответственно, считывать данные.

То есть, вот принцип работы указателей.

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

Если вам кажется сейчас, что вы что-то не улавливаете, то изучите этот вопрос подробнее, прежде чем идти дальше.

Итак, мы теперь с вами готовы записать свою первую программу с использованием указателей.

И пусть она делает ровно то, о чем я вам только что рассказывал.

Меняет значение переменной d через указатель gpt.

Перейдем сюда, в интегрированную среду.

И, соответственно, здесь объявим вот эту переменную d. И пусть она принимает вот это значение 10.

Далее объявляем указатель.

Причем тип данных, с которыми будет работать указатель у нас char, потому что переменная d у нас тоже чаровская.

Ну и далее мы этому указателю должны присвоить адрес вот этой переменной d. Делается это вот таким вот образом, то есть ставится ampersand, операция взятия адреса, и соответственно этот адрес будет присвоен указателю gpt.

Далее давайте с помощью функции print выведем информацию об этом указателе, то есть gpt равняется %p, выведем адрес этого указателя, затем то, что содержит этот указатель, значит %d, ну и то, что содержит переменную d.

Значит %d slash n и здесь мы перечислим все наши переменные g5, потом звездочка g5 и соответственно d. Далее мы изменим значение в ячейке, где хранится переменная d через указатель g5.

Как это делать мы уже с вами знаем.

Ставится звездочка g5 и допустим присваиваем значение 100.

Соответственно, после этого тоже выведем то, что у нас получится Давайте запустим программу и убедимся, что все действительно работает так, как мы это и предполагаем Значит, смотрите, у нас вот здесь вот в начале отображается адрес, где находится вот эта переменная d То есть адрес вот такой вот Далее мы видим, что значение, которое читается с помощью указателя g5 из этой ячейки, равно 10 И переменная d тоже равна 10

Далее мы в эту ячейку заносим новое значение 100.

Соответственно, когда мы его читаем с помощью указателя, то видим ту же самую соточку.

И кроме того, видим, что переменная d тоже изменила свое значение на 100.

То есть вот так вот с помощью указателя мы изменили значение переменной d с 10 на 100.

А теперь смотрите.

Мы с вами вот в этой программе можем объединить две вот эти вот строчки в одну и записать все в таком виде.

gpt, потом сразу идет инициализация и ampersand.

Вот это будет одно и то же.

То есть мы можем сразу прописать вот так вот.

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

А значит присваиваем ему не адрес, а заносим некоторые значения по какому-то адресу.

Но это не так.

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

Когда мы говорили о переменах, я отмечал этот факт.

Если в момент объявления переменных мы им сразу что-либо присваиваем, то отрабатывает операция инициализации, которая в общем случае отличается от операции простого присваивания.

То есть вот здесь вот у нас идет присваивание обычно, а вот здесь вот инициализация.

И эти две операции отрабатывают по-разному.

Вот это нужно иметь в виду.

Значит, здесь я поставлю все в комментарии.

Так вот, при инициализации указателю, именно указателю, присваивается адрес, а не заносится какое-то значение по некоторому адресу.

Вот это нужно иметь в виду.

Поэтому вот такая вот конструкция инициализации будет соответствовать вот такому присваиванию, когда мы присваиваем именно адрес.

Поэтому эти вот две строчки как раз и можно было бы объединить вот так вот в одну.

И это, кстати, частая запись.

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

Давайте запустим программу, убедимся, что все работает.

Да, действительно, мы видим абсолютно такой же результат.

И меняем значение переменной d на 100.

Итак, мы с вами научились объявлять указатель типа char и с его помощью читать и менять значения байтовой переменной.

Давайте теперь посмотрим, что будет происходить, если вместо char прописать, например, тип int.

Значит, соответственно, у переменной тип int и у указателя тоже будет тип int.

И кроме того, здесь мы будем присваивать уже не число 100, а какое-нибудь побольше, допустим 75432.

Давайте запустим программу и видим, что в целом она работает абсолютно так же.

Вот здесь изначально переменная d была равна 10, а потом, когда мы ее изменили с помощью указателя, она стала принимать значение 75432.

И на первый взгляд работа программы никак не изменилась.

Однако вот этот вот тип int отрабатывается на самом деле несколько иначе.

Смотрите, когда срабатывает вот эта вот строчка, то на уровне машинных кодов число 75432 автоматически раскладывается по 4 байтам, а именно 168.38.1.0.

Соответственно эти байты будут записаны в те ячейки, где находится переменная d.

Ну, как пример, пусть это будут вот такие вот ячейки.

И, соответственно, здесь мы видим 168, 38, 1 и 0.

Это как раз значение 75432.

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

А именно...

как 4 последовательных байта.

Например, если следующей строчкой присвоить не вот такое длинное число, а число 1, то все равно будет сформировано 4 байта, а именно 1 0 0 0, которые будут записаны в соответствующие ячейки переменной типа int.

А вот если бы у нас вот здесь вот указатель по-прежнему имел бы тип char,

то мы вот эту единичку записали бы только в эту первую ячейку.

А вот эти три вообще бы не трогали.

То есть, что там было бы записано, так и осталось бы без изменений.

Но так как у нас не тип char, а тип int, то мы работаем сразу с четырьмя ячейками.

И записываем единичку в первую ячейку и три нуля в последующей ячейке.

Вот в этом смысл вот этого типа, который мы прописываем при объявлении указателя.

То есть этот тип говорит, в каком формате следует представлять данные и как их заносить или считывать по указанному адресу.

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

Иначе возможны непредвиденные ошибки или неточности в представлении данных.

На этом завершим наше первое занятие по указателям.

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

На следующем занятии мы продолжим эту тему и окончательно развеем мифы о сложности этой одной из самых простых концепций языка программирования C.