Указатели в C++ для начинающих. Массивы указателей. Указатели на указатели

Внимание! В этой статье будет применяться авторская терминология. Специалисты применяемые мной термины не используют. Список терминов:

  • Инспекция — значит разыменование.
  • Прописант — значит значение, хранимое на адресе, хранимом в указателе. От слова "прописка"
  • Матрёшка — значит указатель на указатель
  • Прописка — значит выделение памяти (new)
  • Выписка — значит очистка памяти (delete)
Указатели в C++ начинающим обычно очень непонятны из-за двойственной природы.
  • Указательная переменная — это переменная, способная хранить адрес памяти.
Эта статья является продолжением темы Указатели в C++ для начинающих. Поверхностное знакомство.
В С++ указательная переменная объявляется с помощью звёздочки:

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

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

Мне мало нравится термин разыменование, для объяснений я подменю его своим авторским термином инспекция. Таким образом использование значения, хранимого внутри сохранённого в указателе адреса, я буду называть инспекцией указателя, но технически верно использовать именно термин разыменование. Фразу "значение, прописанное внутри адреса, хранимого в указателе" писать долго, и большое число слов мешает ясности вашей мысли, поэтому я использую собственное слово прописант; Имейте это в виду.
Для того, чтобы что-то могло прописаться на адресе, сам адрес должен быть обязательно узаконен. Если адрес вне закона, то ничего хорошего не жди:

Вы, как власть имущий над программой, должны для прописанта использовать адрес узаконенный или программой, или непосредственно вами. Когда создаётся переменная любого типа, в тот момент программа самостоятельно выделяет участок памяти под хранение. Любой участок памяти, выделенный самой программой, следует считать законным участком. Таким образом под указательную переменную выделяется отдельный участок памяти. Адрес памяти, используемый переменной, можно узнавать с помощью амперсанда, применяемого к имени переменной, поэтому вы легко можете подсмотреть адрес указательной переменной:

Поскольку указательная переменная является переменной как таковой, у неё можно узнавать её непосредственный адрес. Важно не запутаться в адресе указательной переменной и адресе прописанта. Чтобы понять, как определить законность и незаконность используемого прописантом адреса, вам нужно уловить суть того, что исходный код является обычным текстом, и какие адреса будут использоваться полученной из текста кода программой, компилятор никак предвидеть не может. Под использование внутренних для создаваемой программы переменных компилятор умеет выделять память самостоятельно. В листинге #2 прописант никак не фигурирует и компилятор о нём просто не знает, компилятор знает, что есть указательная переменная p, которая куда-то указывает, и всё. Куда она указывает, зачем она туда указывает.., компилятор мало интересует, в задачи компилятора входит только выделение памяти под саму указательную переменную, всё остальное находится не в юрисдикции компилятора. Под любую собственную программе переменную компилятор выделяет память, поэтому адреса всех переменных, объявленных в программе, считаются законными. "p"-прописант, который согласно листингу #2, является обычным целым числом (тип указательной переменной), адрес этому прописанту никак не выделялся ни программой, ни компилятором. Тут такое дело, что просто используется какой-то адрес из вообще всего адресного пространства: на этом адресе может находится любой компонент извне доступности программы, поэтому программа легко падает.
Запомните! При работе с адресам памяти, программой каждый используемый адрес должен подхватываться и закрепляться за программой. Такой процесс образно можно назвать отжатием программой у операционной системы памяти. Не отжал — не твоё.
Если используются уже отжатые программой у операционной системы участки памяти, то прописанты, использующие те адреса, никаких проблем программе не доставляют. Но если у операционной системы адреса не отжимались, т. е. в коде программы не происходило объявления переменных, то прописанта на такие адреса не пропишешь, необходимо, опять же, отжимать адреса у операционной системы под нужды создаваемой вами программы. Насильное отжатие адресов памяти возможно с помощью операции new. Если вы не регистрировали поселенца в программе, т. е. вы никак не именовали отдельного прописанта, то в случае особой в нём нужды следует использовать операцию new. Тут важно не запутаться. Или вы используете кусочек памяти уже используемый программой, или вы прописываете адрес для нового прописанта. Любой прописанный вами адрес следует убирать при ненужности прописантовой прописки.
  • Всегда, когда используется new, должно быть delete, завершающее потребность адреса прописантом.

Отжимается у операционной системы, конечно же, свободная память. Самый распространённый вопрос всех, наверное, новичков: "Когда нужно использовать delete?". Чтобы понимать нужный момент использования delete, вам необходимо осознавать области действия определённых участков кода. На совсем простом уровне подобной областью является самый обычный блок программы, заключаемый вовнутрь фигурных скобок. Код, заключнный вовнутрь фигурных скобок, как бы не существует вне пределов скобок. С этим новички много раз сталкиваются, поэтому подобное ограничение должно быть вполне себе ясным. Такой же принцип работает и для пары newdelete. Т. е. пара newdelete оказывается непрямым аналогом фигурных скобок, разница лишь в том, что если забыть закрыть фигурную скобку, то код компилироваться не станет, а если забыть delete, то компилятор скомпилирует программу. Если вы поймёте этот момент с областями, то вам проще будет понимать стандартную отговорку, что delete используется, когда указатель уже не нужен. Эта аналогия с фигурными скобками мне представляется очень хорошей и подходящей именно новичкам.
Из-за невнимательности программиста отжатая память может быть не отдана операционной системе, хотя прописант может быть уже совсем не нужен. Когда прописантов мало, это не особо заметно, но то, чего мало, легко может разрастись со скоростью пложения китайцев. Например, при многократном выполнени какого-то участка кода. Во многих примерах, описываемых на просторах интернета, я наблюдал пренебрежение выпиской прописанта. Из-за такой, казалось бы, мелочи, многие новички не научаются правильной выписке. Запомните, что если прописали прописанту адрес, то нужна обязательная выписка прописанта с того адреса по достижению ненужности ему (прописанту) жилья.
Указательная переменная может иметь в себе прописанта, являющегося указательной переменной. Это потому что у любой указательной переменной всегда есть свой собственный адрес.

Сложно вопринимать подобную объяснительную матрёшку человеку. Слишком легко запутаться. Говорят проще:
p1 — это указатель на указатель
p2 — это указатель на указатель на указатель
Меня напрягает немного объясняться словами "указатели на на указатели на указатели, указывающие на что-то". Система прописант в случае многозвёздочности работает плохо, поэтому использую систему "Матрёшка".
В случае необходимости прописки прописанта в двухуровневой матрёшке память, в самом простом виде, выделяется следующим образом:

Сколько бы не было звёздочек, конечное значение всегда будет не указательного типа. Это конечное значение сейчас я буду называть прописантом. Каждая указательная переменная, являющаяся прописантом в указательной переменной, будет называться матрёшкой.
Уровень вложенности матрёшек равен количеству звёздочек. Матрёшка, хранящая внутри себя матрёшку, требует для хранимой в себе матрёшки прописки. В противном случае вложенная матрёшка оказывается нелегалом. Весь процесс происходит по тому же принципу, который описывался ранее. При прописке адреса для каждой новой матрёшки следует указывать количество матрёшек и тип конечного значения, главного прописанта. При уровне вложенности в три матрёшки, простейший случай выделения памяти выглядит следующим образом:

Вы обязательно разберётесь. Примеры на двух и трёх матрёшках показывают общий принцип выделения памяти и очистки выделенной памяти. Очень важно чистить память (делать выписку) в обратном выделению порядке. Т. е. выделение памяти происходит как бы с самой большой матрёшки, хранящей внутри себя все остальные, а очистка памяти выполняется изнутри.
Указатели на указатели хороши тем, что могут использоваться в качестве многомерных массивов. Количество звёздочек может означать мерность массива. Одна звёздочка — одномерный массив, две — двумерный, три — трёхмерный и т. д. Как правило, слишком многомерные массивы никто не использует из-за сложности их обработки. Сложна не столько их обработка, сколько чтение их в пишемом коде. Чтобы использовать указатели на указатели как массивы, память нужно выделять для массивов, т. е. следует использовать операцию индексации []. В остальном всё как с матрёшками:

Так, правда, никто обычно не пишет. Но этот код, листинг #4, вам может стать полезным сведением способа выписки. Матрёшки у нас как бы оказываются массивами. Сами посудите: некоторая матрёшка хранит внутри себя однотипные объекты: матрёшки. На одном уровне вложенности в главной (самой большой) матрёшке оказалось три матрёшки. Фактически это массив из трёх элементов. При этом каждая из трёх матрёшек способна хранить внутри себя других матрёшек, причём каждая из трёх может иметь собственную ёмкость, определяющую степень вложенности. Каждая из трёх матрёшек фактически оказывается массивом, исключением можно назвать матрёшку, хранящую внутри себя главного прописанта. (Примечание. Хотя ничто не запрещает существовать массиву в 1 элемент, в листинге #4 такой элемент не использован как массив в одно значение, а использован как просто значение, p[2]]). Поскольку все матрёшки легко приводятся к типу массив из скольки-то элементов, то выписка этих матрёшек из памяти происходит спообом выписки массивов, т. е. с применением операции []. Одна из матрёшек массивом не оказалась, поэтому для выписки не используется операция [], но при этом вытаскивание главного прописанта происходит при помощи обращения к элементу массива по индексу. Не спутайте пустые квадратные скобки со скобками, в которых указывается число.
В основном, при использовании многомерных массивов, не делают так, чтобы матрёшки содержали внутри себя разные количества матрёшек. Человеческому воображению достаточно легко представлять двумерный массив в виде таблицы, трёхмерный массив в иде куба, но чем больше мерность, тем труднее человеческому мозгу. Все эти указателевы матрёшки массивами вовсе не являются, но общее их представление в конечном итоге выливается в представление массивоподобной структуры, поэтому очень много действий, применяемых к массиву, возможно использовать к указателевым матрёшкам.

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

Каждая матрёшка требует себе прописки в памяти. Об этом уже много раз было написано: или используется уже отжатое у операционной системы программой пространство, или используется ручное отжатие у операционной системы адресного пространства под нужы программы операцией new.
Массив не может хранить в себе массивы, которые отличаются количеством своих элементов, т. е. все массивы в массиве массивов имеют одинаковую ёмкость. Матрёшки не имеют такого ограничения. Как было показано ранее, на одном уровне вложенности может быть несколько матрёшек, каждая из которых различается количеством внутренных своих матрёшек. Но матрёшки требуют выделения себе адресов. Ещё у матрёшек есть преимущество, что ёмкости у них как таковой нет, вы можете отжимать кусочки памяти у операционной системы, а вот с массивами дела похуже, всю доступную в ОС память С++ массив использовать не может.
В современных компиляторах, которые поддерживают стандарт С++11 и выше, возможно инициализировать массивы указателей следующим способом:

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


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

Как вы можете сами увидеть, нельзя в матрёшки второго уровня и выше сохранять многомерные массивы. Причина того только-лишь в том, что массивы не указатели, хотя имена массивов часто неявно преобразовывываются в указательные переменные. Указательная переменная хранит единственное значение — адрес памяти. Массив хранит набор значений.
Цель, которую я преследую, научить вас различать указатели и массивы. Если вы будете говорить правильно, то будете иметь определённый вес в кругах специалистов. Не нужно называть указатели массивами.
Основной причиной невозможности использования указателя на указатель в качестве объекта нацеленного на многомерные массивы является сам способ хранения многомерных массивов в памяти компьютера. Многомерны такие массивы только для нас, для компьютера память линейна, отчего все многомерные массивы в действительности являются одномерными. Если мы можем представить, например, двумерный массив в виде таблицы, как-то обозначить строки: первая, вторая, третья, — то компьютер не различает что где есть. Для компьютера есть начало массива и размер массива, и только одна как бы строчка. Из-за этого все подмассивы многомерных массивов не воспринимаются как отдельные сущности. Все подмассивы являются единым целым, представляющим весь свой массив. Подмассивы они только для нас. Из-за этого, в свою очередь, нигде не хранится адресов на эти подмассивы. А если нет адресов, то и в указатели сохранить нечего.

У массива arr[10][20][30] берётся имя arr, оно неявно приводится к указателю на первый элемент массива, т. е. полученная указательная переменная хранит адрес arr[0]. Именно адрес.
Первые квадратные скобки из arr[10][20][30] вообще отбрасываются. Они указателям не нужны. Не бывает указательных массивов. Бывают только указательные переменные и массивы указателей.
В следствие неявного преобразования имени массива arr к указательной переменной p, у нас на данный момент главенствует не массив arr, а указательная переменная, указывающая на массив [20][30]. Т. е. вне нашего ведома была создана указательная переменная, прописантом которой может стать любой массив из 20 элементов. При этом каждый из 20 прописантов оказывается массивом, хранящим 30 значений.
Преобразованием затронуты только первые скобки: arr[10][20][30], остальные все скобки силу свою не теряют и остаются как есть.
Поскольку неявное преобразование уже выполнялось, вторично оно не происходит: в С++ есть ограничение на количество неявных преобразований за раз.
Есть указательная переменная, указывающая на массив, к указательной переменной иных типов такие переменные неявно не приводятся.

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

Если адреса у некоторой сущности просто физически быть не может, то и сохранять в указатель нечего.
Чтобы различать ссылку и взятие адреса, разделяйте момент зарождения имени переменной и момент использования переменной. В момент зарождения имени знак амперсанда, приставляемый к имени, обозначает то, что имя переменной только-лишь ссылка, а в месте использований имён знак амперсанда, приставляемый к именам, обозначает взятие адреса.
Все комментарии на сайте проверяются, поэтому ваш комментарий может появиться не сразу. Для вставки кода в комментарий используйте теги: [php]ВАШ_КОД[/php]

13 комментариев: Указатели в C++ для начинающих. Массивы указателей. Указатели на указатели

  • Виктория говорит:

    а есть ли разница чему присваивать адрес памяти,разыменованному указателю или просто указателю? у меня в программе например:*ptr=&a;а вы пишете:ptr=&a;

    Автор сайта отвечает:
    Да. *ptr=&a; — В сам элемент с которым работает указатель записано новое значение равное адресу памяти. ptr=&a; — Указатель сменил свои позиции и ссылается на новый элемент, который расположен по адресу на который ссылается a
     
    Виктория, думаю вам полезно будет посмотреть на код
    void main(){
    int a=100;
    int *ptr;
    *ptr=&
    a; //cout<<*ptr<<endl;
    }
    Маленький и простой, но не должен выполнится.
    *ptr=&a; //Тут ошибка. Это вы в int пытаетесь присвоить адрес памяти
    ptr=&a; //Ошибок нет. Так вы сообщаете — меняя указатель — менять переменную int aДело в двойственности указателя. Надеюсь не запутал

  • Anonymous говорит:

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

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

  • yana говорит:

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

    Автор сайта отвечает:
    Этого вот не знаю. Никто не знает, что для вас хорошо и насколько хорошо должно быть описано. Вы уж простите.

  • Аноним говорит:

    В этом коде ошибка

    Указателю нужно сначала присвоить хоть какое-то значение, нужно дописать перед cout строку — ptr = &x;

    Автор сайта отвечает:
    Тут такой момент, что код рабочий может выглядеть так

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

  • Вася говорит:

    Ошибка в коде

    #include
    #include

    int x=20; //Переменная x = 20
    int *ptr; //ptr есть указатель на int

    void main()
    {
    clrscr();
    //Выводим на экран различные значения
    cout<<"ptr = "<<ptr<<endl; //Указатель ptr= Адрес памяти
    cout<<"*ptr"<<*ptr<<endl; //Разыменованный указатель ptr= Значение по адресу
    getch();
    return;
    }
    Для начала нужно присвоить указателю хоть какое-либо значение , нужно дописать перед cout — ptr = &x;

  • Аноним говорит:

    😈 😈 😈 😈 😈 😈 😈 😈

  • Ivan говорит:

    Здравствуйте!
    Я никак не могу понять, что значит &p->name, это значит значение переменной name записать по адресу p? Надеюсь на подсказку

    Автор сайта отвечает:
    нет. это не записать. и где вы такое увидели?.

  • Oksana говорит:

    5 int *ptr; //ptr есть указатель на int
    — неинициализированный указатель !

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

    pnumber = &numberl; // Сохранить адрес в указателе

    Конечно, объявляя указатель, вы можете решить не инициализировать его адресом определенной переменной. В этом случае его можно инициализировать указателем, эквивалентным нулю. Для этого в Visual C++ предусмотрен символ NULL, который уже определен как 0, поэтому указатель можно объявлять и инициализировать с помощью следующего оператора вместо того, что вы видели ранее:

    int* pnumber = NULL; // Указатель, не указывающий ни на что

    (http://develab.narod.ru/cpp/189.htm)

    Автор сайта отвечает:
    Да. неицинализорованный указатель и указывает он на тип int/
    конечно неправильно так говорить, но так его читают.
    по-русски он читается, как: Указатель, указывающий на ячейку памяти, где тип значения чего-то там лежащего должен быть int, но, блин, это долго и длинно.

    из-за того-что в ячейке лежит фиг знает что, и фиг знает какой у этого фига тип, указатель просто может психануть и уронить программу., но может и не уронить.
    там в вашей цитате слово «произвольную», а «совершенно случайную» лучше подходит.

    А вы знаете почему предпочтение отдают инициализацией NULL, а не нулем?
    и еще, чтоб все знали NULL не символ

    • NULL в языках программирования Си и C++ — макрос

    Символ — это один какой-то единый знак, а тут целое слово из 4 символов.

  • прок говорит:

    так как указатель хранит адрес памяти, то попытка обращения к указателю вернет этот адрес. Если x объявлен как указатель, то

    у меня ошибку выводит Ошибка 1 error C4700: использована неинициализированная локальная переменная “tt” c:\users\sррр\documents\visual studio 2013\projects\consoleapplication8\consoleapplication8\consoleapplication8.cpp 9 1 ConsoleApplication8

    • admin говорит:

      Где-то в коде у вас переменная tt
      У меня в примерах такой переменной нету.
      Нужно найти и присвоить ей начальное значение. int tt=0;

  • Валерий говорит:

    Не  могу понять, почему совмещение с инициализацией выглядит так:   тип *ptr=имя переменной. Но ведь указатель под звёздочкой есть величина переменной.

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

  • DN говорит:

  • Джони говорит:

    Всё классно. Разжёвано.. Спасибо

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

Ваш e-mail не будет опубликован.

Поиск

 
     

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

Яндекс.Метрика
НАГРАДИ АВТОРА САЙТА
WEBMONEY
R375024497470
U251140483387
Z301246203264
E149319127674

Мы должны убедиться, что сайтом пользуется не робот!!! Для этого разденьтесь догола и включите wеb-камеру.

Выражаю свою признательность

  • Максиму очень признателен за указание на мои ошибки и неточности.
  • Sergio ===> за оказание помощи в исправлении моих ошибок
  • Gen ===> за правильное стремление помочь другим новичкам и выявления моих ошибок