Указатели в С++ для начинающих. Поверхностное знакомство

Указатель в C++ — это очень простая сущность и очень сложная тема.
  • Указатель — это вид переменной, способной хранить адрес памяти.
Работа компилятора непосредственно связана с памятью компьютера. Новички, изучающие С++, очень быстро сталкиваются с необходимостью использования указателей. Ограничения, задаваемые правилами языка, приводят к тому, что в С++ с указателями знакомиться нужно рано. Задать массиву количество элементов с клавиатуры — используй указатель; создать свой аналог вектора — используй указатель; создать свой аналог string — используй указатель… Указатель, указатель, указатель. Указатель — это всего-лишь тип переменной, но в отличие от самых обычных типов, этот тип составной и имеет полезные особенности. Тип "указатель" — это тип, который способен хранить адрес памяти. Всё, больше указатель хранить ничего не может.
  • Указатели — это хранители адресов.
Компьютер может выделять память блоками разных размеров. Размеры выделенных блоков определяются типами данных. Указатель сам по себе тип данных, но ему, как бы это странным не могло показаться, нужно тоже указывать тип. Тип, задаваемый указателю, даёт знать компилятору размер блока памяти, располагаемого по адресу, хранимому внутри указателя.
  • Объявляется указательная переменная при помощи дописывания звёздочки * к имени переменной. Сама звёздочка располагается между типом и названием переменной. Звёздочка может отделяться пробелами, ни на что, кроме внешнего вида кода, эти пробелы не повлияют.
  • Указательную переменную чаще называют просто "указатель"

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

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

Листинг #1 является примером вывода адреса, хранимого в указателе, на экран. Помните, что я назвал указатели хранителями адресов?
Работа с адресами памяти важна, но ценность указателей в том, что их возможно использовать не только для сохранения адресов, но и для работы с данными, располагающимися на хранимых в них адресах. Этот момент обычно сложно понимают все новички, пришедшие в мир С/С++. Обращение, в общем, возможно двоякое: непосредственно с адресами памяти и со значениями по тем адресам.
  • Есть прямое обращение к указателю и косвенное обращение. При прямом обращении работа происходит с адресами памяти, при косвенном обращении работа происходит со значениями, вытаскиваемыми из адресов. Косвенное обращение называется разыменованием указателя.
Пример прямого обращения к указателю вам был приведён в листинге #1. Косвенное обращение происходит путём добавления звёздочки к имени указателя. При этом важно не запутаться: непосредственно в объявлении звёздочка используется для обозначения того, что переменная указатель, а в дальнейшем любое приписывание звёздочки к имени переменной обозначает косвенное обращение к указателю, т. е. используется значение, которое хранится на хранимом в указателе адресе.
Эта двойственность указателя выносит мозги всем начинающим программистам. Вам нужно самостоятельно научиться понимать описанное различие. Идём дальше.
Указательные переменные можно использовать как обычные переменные, это демонстрируется в листинге #2. При взаимодействии двух однотипных указателей проблем никаких нет.

  • В языке C++ есть операция получения адреса: &.
  • В указатель можно сохранять адрес памяти.
  • Результат операции взятия адреса можно сохранять в указатель.

В листинге #3 используется два вида способа работы с указателем: прямое обращение к указателю для сохранения адреса (8-я строчка) и косвенное обращение для работы со значением с того адреса (10-я строчка). Прямота или косвенность определяется применением звёздочки к указательной переменной: при прямом обращении имя указательной переменной используется без звёздочки, при косвенном используется звёздочка. Говорят, что указатель указывает на что-то. В нашем случае, в листинге #3, указатель p указывает на адрес переменной a, возможно использовать как сам адрес, так и значение с того адреса. Указатели могут указывать на указатели, тогда прямое обращение к указательной переменной не исключает использование звёздочки, ведь в указателе может храниться адрес, на котором прописана указательная переменная. Тут важно не запутаться и не исковеркать доносимую мной информацию. Т. е. получается цепочка: прямое-прямое или прямое-косвенное, или прямое-прямое-прямое или прямое-прямое-косвенное. Там, где вытаскивается значение, там всегда косвенное, после него прямого обращения идти не может. Как иначе объяснить эту часть, мне неведомо.
Очень важно, чтобы указатель указывал на выделенную программой память, в противном случае говорят, что указатель невалиден. Пример невалидного кода:

В листинге #4 приведён пример ошибки, когда указатель указывает на неопределённый участок памяти и в ту часть памяти происходит попытка записи значения. Чтобы указатель не барахлил, обязательно нужно сделать так, чтобы он указывал на зарезервированный программой участок памяти. В листинге #3, например, указатель не без помощи программиста начинает указывать на адрес переменной a, память для которой выделялась программой, поэтому любые изменения значений путём косвенности законны. В #3 косвенные изменения влияют на значение a, потому что адрес переменной a сохранён в указателе.
  • Косвенное изменение указателя затрагивает то, на что указатель указывает.
Косвенное изменение — это когда изменение происходит разыменованным указателем. Но мне слово "разыменование" кажется малосимпатичным для объяснений.

Чем же принципиально различаются листинг #4 и листинг #5? В листинге #4 используется лотерейная указатель-переменная, в указатель-переменную не было сохранено никакого адреса, в ту лотерейную область памяти происходит попытка записи числа 100. В листинге #5 используется известный программе участок памяти. Подобный эффект можно наблюдать и при использовании переменных обычных типов:

Указатель — это тип. А указательная переменная подчиняется тем же правилам, что и обычная переменная. Переменная, она и есть переменная. Листинги #4 и #5 отличаются друг от друга тем, что указатель из #4 не инициализирован никаким адресом и не направлен нами ни на какой адрес.
Идём дальше.
  • Указатель может указывать вникуда (в 0, в NULL или в null_ptr), в таком случае косвенно изменять значения нельзя.


  • Перед использованием указателя указатель обязательно или инициализировать новым адресом, или сохранить в нём адрес выделенный программой. Только соблюдение этого простого условия поможет вам в понимании начала работы с указателями.
Это правило запомнить легко: поскольку указатель хранит в себе адреса, первым делом нужно записать в указательную переменную адрес, только потом можно работать с указательной переменной косвенным образом. При этом нужно понимать, что записываемый в указатель адрес должен быть уже завербован программой. Если косвенная сущность указателя не нужна, то указатель можно сослать на нулевой адрес.
Иногда брать адрес для указателя не из чего. Можно, конечно, создать новую сущность, после чего подобрать её адрес и использовать тот адрес внутри указателя, но подобный подход к написанию кода немного нелепо выглядит. Существует способ вербовки компилятором отдельного участка памяти. Делается такая вербовка с помощью операции new. Ручная вербовка памяти требует обязательной развербовки операцией delete. Происходит подобное управление памятью следующим образом:

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

  • Слово "вербовка", используемое в этой статье, обозначает "выделение памяти". При общении сдругими программистами используйте правильную терминологию: "выделение памяти","очистка памяти".
Выделять память возможно не только для отдельной переменной, но и для целых массивов:

Тут важно не запутаться: для массивной вербовки используют операцию [], причём эта операция используется и для вербовки, и для развербовки. Хотя вы в будущем наверняка повстречаете многих умников, заявляющих о том, что операция [] для очистки памяти необязательна, их слушать не стоит. Для массивов всегда используется []. Во-первых сразу очевидно, что использовался массив, во-вторых в процесс взаимодействия операционной системы и написанной вами программы вовлечены сложные механизмы работы, которые невидимы нам, да и надеяться на операционную систему сильно не стоит. Правила языка C++ строго определяют, что для массивов используется [].
Понимание различия между прямым обращением к указателю и косвенной работы с указателем — прямой путь к пониманию указателей. Вам нужно научиться понимать, когда используется непосредственно адресация, а когда, так сказать, инспекция адреса.
Каждый байт памяти компьютера имеет адрес. Адреса — это те же числа, которые мы используем для домов на улице. Загружаясь в память, наша программа занимает некоторое количество этих адресов. Это означает, что каждая переменная и каждая функция нашей программы начинается с какого-либо конкретного адреса. Реальные адреса, занятые переменными в программе, зависят от многих факторов, таких, как компьютер, на котором запущена программа, размер оперативной памяти, наличие другой программы в памяти и т. д.

  • Запомните, что адреса переменных — это не то же самое, что их значение.
Есть у указателей свои особенности.
Указатель может указывать вникуда.

Указательная переменная может хранит адрес, по которому прописана другая указательная переменная:

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

Существует тип указателя, способного указывать на любой тип данных:

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

Обычно о типе void говорят, что это пустой тип. Вы не можете использовать пустой тип для обычных переменных, но можете применять его к указателю. Пустой тип как-то плохо звучит, вместо слова "пустой" буду комбинировать это слово со словом "неполный", по сути своей оба слова одно: void. О неполных указателях можно написать отдельную статью. Пока что ограничимся оглашёнными на сей момент сведениями.
  • void-указательная переменная — это указательная переменная, которая указывает на адрес памяти, к которому не приписано никакого типа. Отсюда название "пустой" или "нетипизированный указатель".
Указатели применяются для эффективности программ и компактности кода, а в некоторых случаях потому, что банально нет никакой альтернативы. Применяются указатели для организации сложных структур данных: списки, очереди, деревья. Указатели используют для взаимодействия с массивами.
  • Об указателях говорят, что — это верный способ запутать программу до невозможности. Это, несомненно, правда — если пользоваться ими бездумно. Совсем нетрудно так организовать работу с указателем, чтобы он указывал на что попало. Но при тщательном подходе с помощью указателей можно добиться большей ясности и простоты кода, чем без них.
Любую операцию, выполняемую с помощью индексации массива, можно проделать с применением указателей, причем код с применением указателей обычно работает быстрее, но для непосвященных выглядит более запутанно.

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

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

  • Написание массива функций
  • Ручное управление памятью
  • Избежание издержек копирования при работе с параметрами функций
  • Использование полиморфизма в С++
  • Создание сложных структур данных
Подводим итоги:
  • Указатель — это тип.
  • Указательная переменная создаётся с помощью операции *.
  • При объявлении нескольких указателей, каждое отдельное имя переменной требует явного указания того, что она указательная.
  • Указательная переменная — это тип переменной, способной хранить адрес. Чаще указательную переменную называют просто указателем. Отсюда начинают расти ноги проблемы понимания указателей новичками.
  • Указательная переменная имеет двойственную природу.
  • К указательной переменной должен быть приписан какой-либо тип. При этом приписываемый тип может быть типом void, т. е. пустой тип.
  • Тип, приписанный к указательной переменной, должен соответствовать типу значения, которое предполагается использовать для адреса внутри указателя. (кроме типа void)
  • В самую первую очередь: для дружбы с указателем необходимо записать в указатель любой завербованный программой адрес. Иначе указатель обидится и сломает вам программу.
  • Адрес для указателя можно заимствовать у уже существующей переменной, а можно вербовать новый адрес с помощью пар new…delete или new[]…[]delete
  • Использование пар new…delete(new[]…[]delete) требует особой внимательности и правильного управления, иначе можно ловить утечки памяти
  • У указательной переменной есть две сущности: прямая сущность (адрес) и косвенная сущность (значение с того адреса).
  • Работа с косвенной сущностью указателя происходит путём разыменования указательной переменной.
  • Разыменование указательной переменной происходит с помощью операции *
  • Звёздочка может использоваться в момент порождения переменной, тогда она обозначает указательность, в любом случае после этого момента звёздочка, применяемая к имени указательной переменной, будет обозначать косвенную работу с указательной переменнной.
  • Имеется тесная связь между массивами и указателями, но массивы относятся к иному типу данных. В некоторых контекстах имена массивов умеют приводиться к указателям на первый элемент.
  • В умелых руках указатели оказываются мощным средством
Язык С++ напичкан перегруженностью операций. Это помогает нам писать более читаемые и интуитивно понятные коды, но иногда осложняет процесс знакомства с языком. Под перегруженностью операций следует понимать использование одной и той же операции для достижения разных целей. Например, звёздочка, она одновременно может быть и обозначением указательности переменной, и операцией арифметического умножения, и обращением к косвенной сущности указателя. Вон как много действий навешано на один символ. Подобные умения называют перегрузкой. Таким же образом ведёт себя и операция &. В обычных условиях при работе со встроенными переменными эта операция способна быть взятием адреса, обозначением ссылочной переменной, битовым оператором "и". Всё это в совокупности порождает сложную проблему правильного понимания новичками. Если битовое "И" используется в условном операторе if, то взятие адреса можно легко спутать с указательным значением, т. е. с прямым адресом. Ссылки — это отдельные субстанции, которые создаются как псевдонимы оригинальным именам переменным. Вам нужно понять, что есть момент создания, он же момент объявления, он же момент порождения программистом имени. Именно в тот момент определяется, чем переменная окажется в конченом итоге: переменной обычного типа, переменной указательного типа, ссылкой, или переменной типа более сложной структуры данных. Обозначения указательности * или ссылочности & используются только в момент объявления/инициализации. В дальнейшем нет никакого обозначения ни указательности, ни ссылочности, есть только имя переменной, при этом в случае указательной переменной может быть косвенная сущность, а в случае ссылочной переменной ничего больше нет. Поэтому, после процесса объявления/инициализации переменной операция & обозначает только взятие адреса. (хотя может быть битовое &, которое никак никого здесь не путает). Можно говорить о том, что взятие адреса указатель, потому результатом её отдаётся адрес памяти.
Обнуление указателя возможно в нескольких вариантах.

null_ptr предпочтительнее ноля и NULL. Во многих компиляторах 0 и NULL одно и то же, просто использование слова NULL даёт наглядности, что используется указатель. На некоторых компьютерах нулевой адрес может быть задействован для тех же целей, для которых задействуются обычные адреса, а пустым адресом может быть, например, адрес -1. Это редко можно встретить, но тем не менее шансы на это напороться существуют. Поэтому использование null_ptr считается наиболее предпочтительным.
Одно из важных достоинств указателей — это то, что в любой момент времени к ним можно приписывать различные адреса.

Основным недостатком указателей можно считать сложность их чтения людьми:

Также к сложностям стоит отнести произношение при чтении: указатель на указатель на указатель на указатель, указывающий на целое. Человеческая соображалка легко ломается подобными речевыми изысками.

7 комментариев на «“Указатели в С++ для начинающих. Поверхностное знакомство”»

  1. Sergio:

    Помогите найти косяк… Похоже я запутался с указателями. 😳
    Интересуют вопросы в тексте программы, а также самый главный: как сделать так, чтобы закомментированный кусок заработал?

    Извиняюсь за корявые двойные кавычки…

    Автор сайта отвечает:
    вы, похоже, время теряете на то, чтобы каждый раз код руками в комментариях написать. Можно ведь скопировать его из блокнота, только подправлять комментарии нужно будет. (исходники ведь не с воздуха, они на диске хранятся, открыть блокнотом можно) И никаких проблем с самим кодом не будет.
    А иначе и вы и я терем много времени. Чтобы расшифровывать такой вид кода время ведь тоже нужно.

    Как ни странно, но код в комментах никогда руками не набирал, копировал из Borland 5.02 C++.
    [& quot]; — тег html, двойная кавычка.

    Автор сайта отвечает:
    я его расшифровал, знания html небольшие, но есть) За это не парьтесь.
     
    Не ленитесь открывать блокнотом. Оттуда не так криво копирует как из сред разработок.
     
    и такие вот вопросы, как этот, лучше в форумы задавать. Там ответят точно быстрее.
    Разбираться в чужом коде иногда проблема. Стиль написания совсем не мой.

    Почему отображается только первая цифра? Потому что char *mQ[5]; обозначает массив из 5 символов. Т.е. каждый элемент массива вмещает не больше чем 1 символ, массив одиночных символов не есть массив слов.

    Ответ на остальное не знаю когда напишу.

    И правда, не хватает форума на вашем сайте 😀

    Автор сайта отвечает:
    числа это совсем не левые, а коды символов, которые принадлежат элементам массива. Вы же объявили тип int и присваиваете к нему тип char. символу "1" соответствует числовой код 49 и так далее.
     
    Пример прост.

    то же самое происходит у вас.

    Спасибо, с этим вроде разобрался…

    Автор сайта отвечает:
    И в строке 41 попробуйте
    char *B[5] = {0,};

    Пробовал, от «вылетов» и прочих глюков помогло, спасибо.
    Но вот по части if-ов у меня явный косяк, не работает — не выводятся элементы массива указателей… 😕

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

    и еще в глаза бросилось, что присваивание в конце не к разыменованному массиву, а к указателю идет. (это так, просто в глаза бросилось, возможно ошибка, возможно так и задумано, не знаю)

    Это намного упрощенная версия моей программы, поэтому все условия и не писал 😮

    Автор сайта отвечает:
    Так как вы достаточно активно изучаете мои материалы, то держите подсказку. Недавно в одном из комментариев я просил именно вас проверить в вашем проекте что-то похожее (вы там про все способы описания массивов слов спрашивали), но похоже, что вы решили, что вам это не нужно и зря.

    Это относится к строке 42
    ===================

    Это не решение, а подсказка.
     
    и еще вместо приведения типов (int = char) используйте функции работы со строками. Вы же хотели строку к цифре привести, так и используйте готовую для этого функцию. atoi (http://ru.wikipedia.org/wiki/Atoi)
    Пример

    Это вам для решения тоже нужно знать.

    Огроменное спасибо за atoi, если бы раньше знал про её существование, то и не задавал бы глупых вопросов… 😉

    P.S. Хотелось бы в будущем на вашем ресурсе увидеть статью (а может и серию статей): Преобразование типов переменных: int в char, char в int и др.. Новичкам, как мне, это будет крайне полезная информация.
     
    Выложу кусок кода, который работает как изначально и задумывалось, может кому-нибудь пригодится…

     
    admin, честное слово, на этот раз копировал из блокнота))

    Автор сайта отвечает:
    Потом исправлю, только хочу пояснить, что решение какой-то задачи без условия самой задачи новичкам полезным в принципе быть не может.

  2. Александр:

    Здраствуйте.Подскажите,может-ли быть,что запись ( ptr[0] = 100; ptr[1] = 200ptr[2] = 300; ) это запись двух восьмизначных чисел? Первое число это собственно адрес ptr?Второе,соответственно адрес 100,200,300? Это из Листинга #7/

    • 1. Вопрос, возможно, я не понял.
      2. Не может, потому что это запись трёх чисел, в существующем варианте трёх трёхзначных чисел.
      3. Не может, потому что в 2 восьмизначных числах цифр 16, а 9 цифр маловато.
      4. Первое число не может быть адресом, потому что в С++ числовой тип и тип адрес отличаются, а массивы хранят однотипные элементы, т. е. или только числа, или только адреса.

      • Александр:


        Возможно я не точно выразил(я только начал знакомиться с программированием) я представлял,что ptr это адрес-число(0074FE38-допустим),тесть число выраженное в 8 символами и число 100 это адрес,возможно адрес состоит из трех адресов &1-число ,&0-число,&0-число .И сделал предположение.Возможно оно и по сути пустое.Спасибо

        • Ну, оно неверное. Но объяснить мне это сложно. То, что оно всё так не делится, как Вы попробовали себе представить, можете быть уверены.
          ptr в примере — идентификатор адреса. Да.
          100 — не адрес. Это информация, которую хранит память. Некоторое запомненное состояние.
          Адрес — это только идентификатор начала участка памяти, где хранится состояние, которое для нас компьютером представляется в удобном нам виде. Нужно разделять понятие адреса и значения.

  3. Александр:


    Дальше,наверное,лучше не публиковать.Скорее всего точно»туплю»,но ведь значение 100 записано в ячейки с адресами.А сам адрес-это набор символов-число в 16ричной систме счисления.Еще раз спасибо большое за подачу материала.Приму Ваше мнение(позже разберусь с более точным пониманием)и буду дальше изучать тему.Много отвлекаюсь.

    • Это всё (немного условно) выглядит так:
      У памяти есть одна ячейка памяти. В С++ одна ячейка памяти содержит несколько полей, которых должно хватать для 1 байта информации. В наиболее распространённом варианте 1 байт состоит из 8 битов. 1 бит — это в наших архитектурах компьютеров или 0 или 1. Число 100 в двоичной системе 01100100, вот это вот число влезает в одну ячейку памяти (1 символ попадает в одно поле) и умещается во все 8 бит (посчитайте число цифр, их <=8), поэтому ни одна его часть не относится ни к каким адресам, оно само умещено в определённый адрес. Если 8 бит не хватает(число 300 в двоичной системе 0000000100101100), то объединяется несколько ячеек и, наверное (но я не уверен и это неточно), можно говорить, что часть числа находится в отдельном адресе. Но всё число поциферно не расположено в отдельных адресах. (Левые нули незначащие, при счёте цифр их можно откидывать) Очень точно ответить на Ваш вопрос не могу, я не знаю столь низкоуровневой базы, для этого нужно немного почитать учебник информатики.

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

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

Поиск

 
     

Случайная книга в электронном формате

https://www.litres.ru/kim-bentli/upravlenie-elektronnymi-ustroystvami-na-c-22988275/?lfrom=15589587

Последние комментарии

Яндекс.Метрика