Полный обзор того, что такое указатели и как они работают.

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

Переменные, память и адреса

Во-первых, чтобы понять, что такое указатель, мы должны немного разобраться в переменных и памяти. Когда создается переменная, такая как char, short и float, этой переменной выделяется достаточно места для хранения значения этого типа данных переменных. Например, один байт для символов, два байта для коротких строк и 4 байта для int являются общими. Каждый байт памяти имеет уникальный адрес, а назначенный адрес переменной - это место, где хранится первый байт информации.

Чтобы получить адрес переменной в C ++, мы используем оператор & (амперсанд). Допустим, у нас есть int myInt = 4. Чтобы получить доступ к адресу myInt, мы поместим оператор & перед именем переменной .. & myInt. Вместо того, чтобы дать вам 4, которые находятся в памяти, он даст вам местоположение, где эта переменная хранится. Вам будет предоставлен адрес, например 0x8f05.

Передача по значению и передача по ссылке

Я считаю, что очень важно точно понимать, что происходит, когда вы передаете переменную или объекты на конкретном языке, с которым работаете. Они разные в зависимости от языка. В C ++ переменные и объекты передаются по значению. Легко подумать об этом, когда вы передаете переменную функции, вы фактически передаете копию этой переменной. Эта копия переменной не влияет на аргумент, который вы передали в функцию. Теперь это две совершенно разные переменные. Копия является локальной только для функции. Эта копия имеет связанный срок хранения и будет освобождена после того, как функция выполнит свой курс.

Когда что-то передается по ссылке, оно не копируется, но вы фактически передаете ссылку на исходные данные. Таким образом, данные в основном имеют два имени, но оба «указывают» на один и тот же фрагмент данных. На приведенной ниже иллюстрации myInt был исходной переменной, содержащей int 4. Когда вы передаете myInt в функцию, вы затем будете ссылаться на эти данные по имени параметра. В этом сценарии мы используем myReference. Когда вы используете myReference, это напрямую влияет на данные в myInt, и теперь они оба имеют доступ к этим данным. Вы будете ссылаться на эти данные как на myReference только локально внутри функции.

Итак, если переменные передаются по значению, зачем объяснять передачу по ссылке? Что ж, во-первых, было бы хорошо понять, как именно они работают. Чем быстрее вы получите четкое представление об этих концепциях, тем легче станет ваша жизнь. Во-вторых, хотя в C ++ переменные передаются по значению, существуют способы явной передачи переменных по ссылке. Помните, ранее я объяснял использование оператора & перед тем, как имена переменных возвращают адрес? Вы можете использовать тот же оператор в своих параметрах функции, чтобы принять адрес аргумента. Это значение будет передано по ссылке, и все, что вы сделаете с этими данными, также повлияет на данные в исходном, поскольку они оба «указывают» на одни и те же данные. Хотя указатели и ссылочные переменные кажутся похожими, есть некоторые различия, такие как арифметика указателей.

Что такое указатель?

Ну, указатель на самом деле является переменной-указателем. Если вы помните, что это переменная, это может упростить понимание. Указатель предназначен для хранения адресов памяти. Указатель имени довольно четкий. Он «указывает» на данные, хранящиеся в памяти компьютера. Это позволяет вам косвенно работать с данными, на которые они указывают.

Определение указателя очень похоже на любое другое определение: int * ptr. * (звездочка) перед ptr указывает, что ptr является переменной-указателем, а int указывает, что ptr будет указывать на целое число. Никогда не стоит определять переменную-указатель без ее инициализации. При необходимости вы можете присвоить ему nullptr, например int * ptr = nullptr. Это назначит указатель на адрес 0. Теперь, возвращаясь с помощью оператора & для получения адреса, чтобы назначить указатель на переменную, вы должны использовать int * ptr = & myInt. Это присваивает адрес myInt переменной-указателю ptr. Оба myInt и ptr указывают на один и тот же адрес, содержащий данные в myInt. Итак, после того, как вы назначите ptr для myInt, если вы использовали ptr = 5, вы назначите 5 для myInt. Чтобы получить доступ к данным, на которые ссылается указатель, мы помещаем перед указателем *. Это называется косвенным оператором. Используя * ptr, он разыменует указатель и вернет 5 вместо адреса.

Рассмотрим массив. Когда массив передается в функцию, вы фактически передаете не массив, а его начальный адрес. Переменная массива с именем myArr, переданная в функцию с именем параметра myReferemce, будет указывать на массив myArr. Фактически, вы не можете передать массив по значению в C ++ из-за ограничения C (C ++ является производным от C). Посмотрите на пример ниже, когда вы создаете функцию с формальным параметром для массива, она принимает начальный адрес переданного в нее адреса.

Указатель арифметики

Имя массива без скобок или нижнего индекса представляет начальный адрес массива. При разыменовании myArray он вернет то, что хранится по начальному адресу этого массива, * myArr == myArr [0]. Массив хранится в памяти в последовательном порядке. Чтобы получить доступ к содержимому массива с помощью оператора косвенного обращения, нам нужно добавить или вычесть, чтобы получить адреса других элементов. Например, * (myArr + 1) вернет вам данные, хранящиеся в myArr [1]. За кадром на самом деле происходит myArr + 1 * 4. Когда вы добавляете или вычитаете число из разыменованного массива, оно будет увеличиваться или уменьшаться в этом массиве на размер того, что там хранится. Итак, поскольку наш массив представляет собой int, мы можем предположить, что размер, выделенный для каждого элемента, будет равен 4. Каждое приращение будет перемещать 4 байта от начальной точки к следующему адресу, где хранится это int. Несколько замечаний:

  • myArr [индекс] == * (myArr + индекс)
  • * (myArr + 1)! = * myArr + 1… Без круглых скобок myArr будет разыменован, и к сохраненному значению будет добавлена ​​1.
  • Помните, что в C ++ массивы специально не обрабатывают доступ к недопустимому индексу. Это классифицируется как неопределенное поведение, и на самом деле может произойти все, что угодно, поэтому обязательно следите за тем, что вы делаете.

Указатели как параметры функции

Когда вы создаете функцию, которая использует параметр указателя, вы должны написать его, как указано выше. Вы должны указать, что параметр этой функции является указателем на int. Затем вы можете передать адрес номера с помощью оператора &. Внутри функции значение разыменяется и умножается на 2. Это также косвенно изменяет количество данных, поскольку они оба указывают на одни и те же данные в памяти.

Константы

Это может показаться сбивающим с толку, но я расскажу как можно проще. Есть 3 отношения:

  • Указатели на константы

В функции const int определяет, на что указывает значение. Константа применяется к тому, на что указывает значение, а не к самому значению (обратите внимание, что определение стоит перед звездочкой). Также звездочка указывает, что значение является указателем. Как и все константы, компилятор не позволяет нам писать код, который изменяет значения, на которые указывают значения. Хотя данные, на которые указывает указатель, не могут изменить, сам указатель может измениться. Кроме того, адрес константы может быть передан только указателю на константу, указатель на константу может получить адрес непостоянный элемент.

  • Постоянные указатели

Ранее мы обсуждали указатель, указывающий на константу, но константный указатель описывает сам указатель. Когда постоянный указатель инициализируется адресом, он не может указывать ни на что другое. Обратите внимание, что на этот раз определение const идет после звездочки. Это поможет вам узнать, что слово const определяет указатель, а не то, на что указывает указатель. Хотя указатель на константу не может быть изменен, данные, на которые он указывает, можно изменить, поскольку число не является константой.

  • Константы Указатели на константы

Если вы понимаете предыдущие отношения, это должно быть вам знакомо. Это просто комбинация двух. Const int (перед звездочкой) описывает, на что указывает ptr. Const ptr (после звездочки) описывает сам указатель. Помните, что указатели на константы могут принимать адреса неконстант. Несмотря на то, что число не является константой, мы объявили, что this указывает на константу, поэтому у этого указателя нет разрешения на изменение значения, на которое он указывает. Кроме того, мы объявили это постоянным указателем, поэтому мы не можем изменить адрес, на который указывает этот указатель.

Распределение динамической памяти

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

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

Вы уже знакомы с тем, как создать указатель: int * ptr = nullptr (не забудьте всегда инициализировать указатель). Чтобы выделить место для переменной, вы будете использовать ключевое слово «новое», за которым следует тип данных. Например, int * ptr = new int или, если указатель уже создан, вы можете просто использовать ptr = new int. Этот недавно назначенный адрес теперь сохраняется в указателе, и к нему можно легко получить доступ, разыменовав указатель с помощью *.

Ниже приведен пример ввода пользователем количества идентификационных номеров, которые должны быть введены в массив. Затем создается указатель и новый массив с входной переменной пользователя, чтобы указать точный размер массива.

Без указателя и распределения динамической памяти это было бы невозможно. Обычно массив, созданный при нормальных обстоятельствах, должен использовать константу для объявления размера массива. Здесь мы смогли сделать это на лету. Несколько замечаний. Если для выполнения запроса недостаточно свободной памяти, C ++ выдаст исключение и завершит программу.

После использования важно удалить или освободить память. Это поможет предотвратить утечку памяти. В C ++ нет сборки мусора. Сборка мусора - это механизм, который восстанавливает данные из кучи, которые больше не используются. Поскольку C ++ не предлагает эту утилиту, решать вам. Использование delete ptr (одиночная переменная), или delete [] ptr (массив) освободит эту память и освободит это пространство. В C ++ также есть интеллектуальные указатели. Это объекты, которые работают аналогично указателям, но могут автоматически удалять выделенную память, когда она больше не используется.

Заключительные мысли

Я надеюсь, что это смогло предоставить некоторую полезную информацию и устранить любую путаницу. Указатели - важная часть программирования на C ++, и твердое понимание этого будет полезным. Я не специалист по этому вопросу, но это всего лишь мое функциональное понимание темы. Обобщение этой информации своими словами и мыслями позволяет мне задуматься над тем, что я на самом деле понимаю. Для меня это такой же обучающий инструмент, как и для всех, кому он нужен. Если у вас есть какие-либо вопросы или, возможно, мне нужно внести некоторые исправления, не стесняйтесь оставить мне комментарий или отправить мне сообщение.