Для чего нужен указатель в c
Указатели
Указатели
Э то, пожалуй, самая сложная и самая важная тема во всём курсе. Без понимания указателей дальнейшее изучении си будет бессмысленным. Указатели – очень простая концепция, очень логичная, но требующая внимания к деталям.
Определение
У казатель – это переменная, которая хранит адрес области памяти. Указатель, как и переменная, имеет тип. Синтаксис объявления указателей
Например
float *a;
long long *b;
Два основных оператора для работы с указателями – это оператор & взятия адреса, и оператор * разыменования. Рассмотрим простой пример.
Рассмотрим код внимательно, ещё раз
Была объявлена переменная с именем A. Она располагается по какому-то адресу в памяти. По этому адресу хранится значение 100.
Создали указатель типа int.
Теперь переменная p хранит адрес переменной A. Используя оператор * мы получаем доступ до содержимого переменной A.
Чтобы изменить содержимое, пишем
После этого значение A также изменено, так как она указывает на ту же область памяти. Ничего сложного.
Теперь другой важный пример
Будет выведено
4
4
8
4
Несмотря на то, что переменные имеют разный тип и размер, указатели на них имеют один размер. Действительно, если указатели хранят адреса, то они должны быть целочисленного типа. Так и есть, указатель сам по себе хранится в переменной типа size_t (а также ptrdiff_t), это тип, который ведёт себя как целочисленный, однако его размер зависит от разрядности системы. В большинстве случаев разницы между ними нет. Зачем тогда указателю нужен тип?
Арифметика указателей
В о-первых, указателю нужен тип для того, чтобы корректно работала операция разыменования (получения содержимого по адресу). Если указатель хранит адрес переменной, необходимо знать, сколько байт нужно взять, начиная от этого адреса, чтобы получить всю переменную.
Во-вторых, указатели поддерживают арифметические операции. Для их выполнения необходимо знать размер.
операция + N сдвигает указатель вперёд на N*sizeof(тип) байт.
Например, если указатель int *p; хранит адрес CC02, то после p += 10; он будет хранить адрес СС02 + sizeof(int)*10 = CC02 + 28 = CC2A (Все операции выполняются в шестнадцатиричном формате). Пусть мы создали указатель на начало массива. После этого мы можем «двигаться» по этому массиву, получая доступ до отдельных элементов.
Заметьте, каким образом мы получили адрес первого элемента массива
Массив, по сути, сам является указателем, поэтому не нужно использовать оператор &. Мы можем переписать пример по-другому
Если же указатели равны, то они указывают на одну и ту же область памяти.
Указатель на указатель
У казатель хранит адрес области памяти. Можно создать указатель на указатель, тогда он будет хранить адрес указателя и сможет обращаться к его содержимому. Указатель на указатель определяется как
Очевидно, ничто не мешает создать и указатель на указатель на указатель, и указатель на указатель на указатель на указатель и так далее. Это нам понадобится при работе с двумерными и многомерными массивами. А вот простой пример, как можно работать с указателем на указатель.
Указатели и приведение типов
Т ак как указатель хранит адрес, можно кастовать его до другого типа. Это может понадобиться, например, если мы хотим взять часть переменной, или если мы знаем, что переменная хранит нужный нам тип.
В этом примере мы пользуемся тем, что размер типа int равен 4 байта, а char 1 байт. За счёт этого, получив адрес первого байта, можно пройти по остальным байтам числа и вывести их содержимое.
У казатель до инициализации хранит мусор, как и любая другая переменная. Но в то же время, этот «мусор» вполне может оказаться валидным адресом. Пусть, к примеру, у нас есть указатель. Каким образом узнать, инициализирован он или нет? В общем случае никак. Для решения этой проблемы был введён макрос NULL библиотеки stdlib.
Принято при определении указателя, если он не инициализируется конкретным значением, делать его равным NULL.
По стандарту гарантировано, что в этом случае указатель равен NULL, и равен нулю, и может быть использован как булево значение false. Хотя в зависимости от реализации NULL может и не быть равным 0 (в смысле, не равен нулю в побитовом представлении, как например, int или float).
Это значит, что в данном случае
вполне корректная операция, а в случае
поведение не определено. То есть указатель можно сравнивать с нулём, или с NULL, но нельзя NULL сравнивать с переменной целого типа или типа с плавающей точкой.
Примеры
Теперь несколько примеров работы с указателями
1. Пройдём по массиву и найдём все чётные элементы.
2. Когда мы сортируем элементы часто приходится их перемещать. Если объект занимает много места, то операция обмена местами двух элементов будет дорогостоящей. Вместо этого можно создать массив указателей на исходные элементы и отсортировать его. Так как размер указателей меньше, чем размер элементов целевого массива, то и сортировка будет происходить быстрее. Кроме того, массив не будет изменён, часто это важно.
3. Более интересный пример. Так как размер типа char всегда равен 1 байт, то с его помощью можно реализовать операцию swap – обмена местами содержимого двух переменных.
В этом примере можно поменять тип переменных a и b на double или любой другой (с соответствующим изменением вывода и вызова sizeof), всё равно мы будет обменивать местами байты двух переменных.
4. Найдём длину строки, введённой пользователем, используя указатель
Урок №80. Указатели
На уроке №10 мы узнали, что переменная — это название кусочка памяти, который содержит значение.
Оператор адреса &
При выполнении инициализации переменной, ей автоматически присваивается свободный адрес памяти, и, любое значение, которое мы присваиваем переменной, сохраняется по этому адресу в памяти. Например:
При выполнении этого стейтмента процессором, выделяется часть оперативной памяти. В качестве примера предположим, что переменной b присваивается ячейка памяти под номером 150. Всякий раз, когда программа встречает переменную b в выражении или в стейтменте, она понимает, что для того, чтобы получить значение — ей нужно заглянуть в ячейку памяти под номером 150.
Хорошая новость — нам не нужно беспокоиться о том, какие конкретно адреса памяти выделены для определенных переменных. Мы просто ссылаемся на переменную через присвоенный ей идентификатор, а компилятор конвертирует это имя в соответствующий адрес памяти. Однако этот подход имеет некоторые ограничения, которые мы обсудим на этом и следующих уроках.
Оператор адреса & позволяет узнать, какой адрес памяти присвоен определенной переменной. Всё довольно просто:
Результат на моем компьютере:
Примечание: Хотя оператор адреса выглядит так же, как оператор побитового И, отличить их можно по тому, что оператор адреса является унарным оператором, а оператор побитового И — бинарным оператором.
Оператор разыменования *
Оператор разыменования * позволяет получить значение по указанному адресу:
Результат на моем компьютере:
Примечание: Хотя оператор разыменования выглядит так же, как и оператор умножения, отличить их можно по тому, что оператор разыменования — унарный, а оператор умножения — бинарный.
Указатели
Теперь, когда мы уже знаем об операторах адреса и разыменования, мы можем поговорить об указателях.
Указатель — это переменная, значением которой является адрес ячейки памяти. Указатели объявляются точно так же, как и обычные переменные, только со звёздочкой между типом данных и идентификатором:
Синтаксически язык C++ принимает объявление указателя, когда звёздочка находится рядом с типом данных, с идентификатором или даже посередине. Обратите внимание, эта звёздочка не является оператором разыменования. Это всего лишь часть синтаксиса объявления указателя.
Однако, при объявлении нескольких указателей, звёздочка должна находиться возле каждого идентификатора. Это легко забыть, если вы привыкли указывать звёздочку возле типа данных, а не возле имени переменной. Например:
По этой причине, при объявлении указателя, рекомендуется указывать звёздочку возле имени переменной. Как и обычные переменные, указатели не инициализируются при объявлении. Содержимым неинициализированного указателя является обычный мусор.
Присваивание значений указателю
Поскольку указатели содержат только адреса, то при присваивании указателю значения — это значение должно быть адресом. Для получения адреса переменной используется оператор адреса:
Приведенное выше можно проиллюстрировать следующим образом:
Еще очень часто можно увидеть следующее:
Результат на моем компьютере:
Тип указателя должен соответствовать типу переменной, на которую он указывает:
Следующее не является допустимым:
Это связано с тем, что указатели могут содержать только адреса, а целочисленный литерал 7 не имеет адреса памяти. Если вы все же сделаете это, то компилятор сообщит вам, что он не может преобразовать целочисленное значение в целочисленный указатель.
Язык C++ также не позволит вам напрямую присваивать адреса памяти указателю:
Оператор адреса возвращает указатель
Стоит отметить, что оператор адреса & не возвращает адрес своего операнда в качестве литерала. Вместо этого он возвращает указатель, содержащий адрес операнда, тип которого получен из аргумента (например, адрес переменной типа int передается как адрес указателя на значение типа int):
Результат выполнения программы:
Разыменование указателей
Как только у нас есть указатель, указывающий на что-либо, мы можем его разыменовать, чтобы получить значение, на которое он указывает. Разыменованный указатель — это содержимое ячейки памяти, на которую он указывает:
0034FD90
5
0034FD90
5
Вот почему указатели должны иметь тип данных. Без типа указатель не знал бы, как интерпретировать содержимое, на которое он указывает (при разыменовании). Также, поэтому и должны совпадать тип указателя с типом переменной. Если они не совпадают, то указатель при разыменовании может неправильно интерпретировать биты (например, вместо типа double использовать тип int).
Одному указателю можно присваивать разные значения:
Когда адрес значения переменной присвоен указателю, то выполняется следующее:
ptr — это то же самое, что и &value ;
Разыменование некорректных указателей
Указатели в языке C++ по своей природе являются небезопасными, а их неправильное использование — один из лучших способов получить сбой программы.
При разыменовании указателя, программа пытается перейти в ячейку памяти, которая хранится в указателе и извлечь содержимое этой ячейки. По соображениям безопасности современные операционные системы (ОС) запускают программы в песочнице для предотвращения их неправильного взаимодействия с другими программами и для защиты стабильности самой операционной системы. Если программа попытается получить доступ к ячейке памяти, не выделенной для нее операционной системой, то ОС сразу завершит выполнение этой программы.
Следующая программа хорошо иллюстрирует вышесказанное. При запуске вы получите сбой (попробуйте, ничего страшного с вашим компьютером не произойдет):
Размер указателей
Размер указателя зависит от архитектуры, на которой скомпилирован исполняемый файл: 32-битный исполняемый файл использует 32-битные адреса памяти. Следовательно, указатель на 32-битном устройстве занимает 32 бита (4 байта). С 64-битным исполняемым файлом указатель будет занимать 64 бита (8 байт). И это вне зависимости от того, на что указывает указатель:
Как вы можете видеть, размер указателя всегда один и тот же. Это связано с тем, что указатель — это всего лишь адрес памяти, а количество бит, необходимое для доступа к адресу памяти на определенном устройстве, — всегда постоянное.
В чём польза указателей?
Сейчас вы можете подумать, что указатели являются непрактичными и вообще ненужными. Зачем использовать указатель, если мы можем использовать исходную переменную?
Однако, оказывается, указатели полезны в следующих случаях:
Случай №1: Массивы реализованы с помощью указателей. Указатели могут использоваться для итерации по массиву.
Случай №2: Они являются единственным способом динамического выделения памяти в C++. Это, безусловно, самый распространенный вариант использования указателей.
Случай №3: Они могут использоваться для передачи большого количества данных в функцию без копирования этих данных.
Случай №4: Они могут использоваться для передачи одной функции в качестве параметра другой функции.
Случай №5: Они используются для достижения полиморфизма при работе с наследованием.
Случай №6: Они могут использоваться для представления одной структуры/класса в другой структуре/классе, формируя, таким образом, целые цепочки.
Указатели применяются во многих случаях. Не волнуйтесь, если вы многого не понимаете из вышесказанного. Теперь, когда мы разобрались с указателями на базовом уровне, мы можем начать углубляться в отдельные случаи, в которых они полезны, что мы и сделаем на последующих уроках.
Заключение
Указатели — это переменные, которые содержат адреса памяти. Их можно разыменовать с помощью оператора разыменования * для извлечения значений, хранимых по адресу памяти. Разыменование указателя, значением которого является мусор, приведет к сбою в вашей программе.
Совет: При объявлении указателя указывайте звёздочку возле имени переменной.
Задание №1
Какие значения мы получим в результате выполнения следующей программы (предположим, что это 32-битное устройство, и тип short занимает 2 байта):
Указатели в C++: зачем нужны, когда использовать и чем отличаются
Память компьютера — это длинный ряд ячеек. Размер каждой ячейки называется байтом. Байт — это пространство, занятое английским символом алфавита. Объект в обычном понимании — это последовательный набор байтов в памяти. Каждая ячейка имеет адрес, который представляет собой целое число, обычно записанное в шестнадцатеричной форме. Есть три способа доступа к объекту в памяти. Доступ к объекту можно получить с помощью так называемого указателя. Доступ к нему можно получить, используя так называемую ссылку. К нему по-прежнему можно получить доступ с помощью идентификатора. В этой статье основное внимание уделяется использованию указателей и ссылок. В C ++ есть заостренный объект и объект-указатель. У остроконечного предмета есть интересующий предмет. Объект-указатель имеет адрес указанного объекта.
Вам необходимо иметь базовые знания C ++, включая его идентификаторы, функции и массивы; чтобы понять эту статью.
У объекта-указателя и объекта-указателя есть свой идентификатор.
Оператор Address-Of, &
Это унарный оператор. Если за ним следует идентификатор, он возвращает адрес объекта идентификатора. Рассмотрим следующее объявление:
Ниже приведен код, следующее выражение, вернет адрес, идентифицированный ptdInt:
Вам не нужно знать точный адрес (номер) при кодировании.
Оператор косвенного обращения, *
Это унарный оператор в контексте указателей. Обычно он печатается перед идентификатором. Если используется в объявлении идентификатора, то идентификатор является объектом-указателем, который содержит только адрес указанного объекта. Если используется перед идентификатором объекта-указателя, чтобы что-то вернуть, то возвращаемое значение является значением указанного объекта.
Создание указателя
Взгляните на следующий фрагмент кода:
Сегмент начинается с объявления указанного объекта ptdFloat. ptdFloat — это идентификатор, который просто идентифицирует объект с плавающей запятой. Ему мог быть присвоен реальный объект (значение), но в этом случае ему ничего не было присвоено. Далее в сегменте идет объявление объекта-указателя. Оператор косвенного обращения перед этим идентификатором означает, что он должен содержать адрес указанного объекта. Тип объекта, плавающий в начале оператора, означает, что заостренный объект является плавающим. Объект-указатель всегда имеет тот же тип, что и заостренный объект. ptrFoat — это идентификатор, который просто идентифицирует объект-указатель.
В последнем операторе кода адрес указанного объекта присваивается объекту-указателю. Обратите внимание на использование оператора адресации &.
Последний оператор (строка) выше показывает, что после объявления объекта-указателя без инициализации вам не нужен оператор косвенного обращения, когда вам нужно его инициализировать. Фактически, использование оператора косвенного обращения в третьей (последней) строке является синтаксической ошибкой.
Объект-указатель может быть объявлен и инициализирован указанным объектом в одной инструкции, как показано ниже:
Первая строка предыдущего сегмента кода и эта совпадают. Здесь вторая и третья строки предыдущего сегмента кода объединены в один оператор.
Обратите внимание, что в приведенном выше коде при объявлении и инициализации объекта-указателя необходимо использовать оператор косвенного обращения. Однако он не используется, если инициализация должна быть выполнена позже. Объект-указатель инициализируется адресом указанного объекта.
В следующем сегменте кода оператор косвенного обращения используется для возврата содержимого указанного объекта.
В последнем предложении здесь оператор косвенного обращения использовался для возврата значения, на которое указывает идентификатор указателя. Таким образом, при использовании в объявлении идентификатор для оператора косвенного обращения будет содержать адрес указанного объекта. При использовании в выражении возврата в сочетании с идентификатором указателя оператор косвенного обращения возвращает значение указанного объекта.
Присвоение нуля указателю
Объект-указатель всегда должен иметь тип указанного объекта. При объявлении объекта-указателя должен использоваться тип данных указанного объекта. Однако значение десятичного нуля может быть присвоено указателю, как в следующем сегменте кода:
int ptdInt = 5 ;
int * ptrInt = ;
В любом случае указатель (идентификатор) называется нулевым указателем; это означает, что он указывает в никуда. То есть у него нет адреса какого-либо указанного объекта. Здесь 0 — это десятичный ноль, а не шестнадцатеричный ноль. Шестнадцатеричный ноль будет указывать на первый адрес памяти компьютера.
Не пытайтесь получить значение, на которое указывает нулевой указатель. Если вы попробуете это сделать, программа может скомпилироваться, но не запуститься.
Имя массива как постоянный указатель
Рассмотрим следующий массив:
Имя массива arr на самом деле является идентификатором, который имеет адрес первого элемента массива. Следующее выражение возвращает первое значение в массиве:
В случае с массивом оператор приращения ++ ведет себя иначе. Вместо добавления 1 он заменяет адрес указателя на адрес следующего элемента в массиве. Однако имя массива — это постоянный указатель; это означает, что его содержимое (адрес) не может быть изменено или увеличено. Итак, для увеличения начальный адрес массива должен быть назначен непостоянному указателю следующим образом:
Теперь ptr можно увеличивать, чтобы указывать на следующий элемент массива. ptr был объявлен здесь как объект-указатель. Без * здесь не было бы указателя; это будет идентификатор для хранения объекта типа int, а не для хранения адреса памяти.
Следующий сегмент кода, наконец, указывает на четвертый элемент:
Следующий код выводит четвертое значение массива:
Имя функции как идентификатор
Имя функции — это идентификатор функции. Рассмотрим следующее определение функции:
fn — идентификатор функции. Выражение,
возвращает адрес функции в памяти. fn похожа на заостренный объект. Следующее объявление объявляет указатель на функцию:
Идентификатор указанного объекта и идентификатор объекта-указателя различаются. func — это указатель на функцию. fn — идентификатор функции. Итак, func может указывать на fn следующим образом:
Значение (содержание) func — это адрес fn. Два идентификатора могли быть связаны с помощью оператора инициализации следующим образом:
Обратите внимание на различия и сходства в обработке указателей функций и скалярных указателей. func — указатель на функцию; это заостренный объект; он объявлен иначе, чем скалярный указатель.
Функцию можно вызвать с помощью,
Его нельзя вызвать с помощью * func ().
Когда функция имеет параметры, во вторых скобках указаны типы параметров, и для них не обязательно должны быть идентификаторы. Следующая программа иллюстрирует это:
#include
using namespace std ;
C++ Reference
Ссылка в C ++ — это просто способ создать синоним (другое имя) для идентификатора. Он использует оператор &, но не так, как & используется для указателей. Рассмотрим следующий фрагмент кода:
cout myInt ‘ \n ‘ ;
cout yourInt ‘ \n ‘ ;
Первый оператор инициализирует идентификатор myInt; т.е. myInt объявлен и содержит значение 8. Второй оператор создает новый идентификатор yourInt, синоним myInt. Для этого в объявлении между типом данных и новым идентификатором помещается оператор &. Операторы cout показывают, что два идентификатора являются синонимами. Чтобы вернуть значение в этом случае, вам не нужно ставить перед ним *. Просто используйте идентификатор.
Здесь myInt и yourInt — это не два разных объекта. Это два разных идентификатора, которые ссылаются (идентифицируют) одно и то же место в памяти, имеющее значение 8. Если значение myInt изменяется, значение yourInt также изменится автоматически. Если значение yourInt изменится, значение myInt также изменится автоматически.
Ссылка на функцию
Так же, как у вас может быть ссылка на скаляр, вы также можете иметь ссылку на функцию. Однако кодирование ссылки на функцию отличается от кодирования ссылки на скаляр. Следующая программа иллюстрирует это:
Обратите внимание на первый оператор в функции main, который делает func синонимом fn. Оба ссылаются на одну и ту же функцию. Обратите внимание на одноразовое использование и положение &. Таким образом, & является здесь ссылочным оператором, а не оператором адресации. Чтобы вызвать функцию, просто используйте любое имя.
Идентификатор ссылки — это не то же самое, что идентификатор указателя.
Функция, возвращающая указатель
В следующей программе функция возвращает указатель, который является адресом указанного объекта:
Первый оператор в функции fn () предназначен только для создания объекта-указателя. Обратите внимание на одноразовое использование и положение * в сигнатуре функции. Также обратите внимание, как указатель (адрес) был получен в функции main () другим объектом-указателем.
Функция, возвращающая ссылку
В следующей программе функция возвращает ссылку:
Первый оператор в функции fn () предназначен только для создания ссылки. Обратите внимание на одноразовое использование и положение & в сигнатуре функции. Также обратите внимание, как ссылка была получена в функции main () по другой ссылке.
Передача указателя на функцию
В следующей программе указатель, который на самом деле является адресом объекта с плавающей точкой, отправляется в качестве аргумента функции:
int main ( )
<
float v = 2.5 ;
Обратите внимание на использование и положение * для параметра с плавающей запятой в сигнатуре функции. Как только начинается вычисление функции fn (), делается следующий оператор:
Передача ссылки на функцию
В следующей программе ссылка отправляется в качестве аргумента функции:
int main ( )
<
float v = 2.5 ;
Обратите внимание на использование и положение & для параметра float в сигнатуре функции. Как только начинается вычисление функции fn (), делается следующий оператор:
Передача массива функции
Следующая программа показывает, как передать массив функции:
int fn ( int arra [ ] )
<
return arra [ 2 ] ;
>
В этой программе передается массив. Обратите внимание, что параметр сигнатуры функции имеет объявление пустого массива. Аргументом в вызове функции является только имя созданного массива.
Может ли функция C ++ вернуть массив?
Функция в C ++ может возвращать значение массива, но не может возвращать массив. Компиляция следующей программы приводит к сообщению об ошибке:
int fn ( int arra [ ] )
<
return arra ;
>
Указатель указателя
Указатель может указывать на другой указатель. То есть объект-указатель может иметь адрес другого объекта-указателя. Они по-прежнему должны быть одного типа. Следующий фрагмент кода иллюстрирует это:
В объявлении указателя на указатель используется двойной *. Чтобы вернуть значение последнего заостренного объекта, по-прежнему используется двойной *.
Массив указателей
Следующая программа показывает, как кодировать массив указателей:
Обратите внимание на использование и положение * в объявлении массива. Обратите внимание на использование * при возврате значения в массиве. С указателями указателей задействованы два *. В случае массива указателей об одном * уже позаботились, потому что идентификатор массива является указателем.
Массив строк переменной длины
Строковый литерал — это константа, возвращающая указатель. Массив строк переменной длины — это массив указателей. Каждое значение в массиве является указателем. Указатели — это адреса к ячейкам памяти, они имеют одинаковый размер. Строки разной длины находятся в другом месте памяти, а не в массиве. Следующая программа иллюстрирует использование:
На выходе получается «girl».
Объявление массива начинается с зарезервированного слова «const» для константы; за которым следует «char» для символа, затем звездочка *, чтобы указать, что каждый элемент является указателем. Чтобы вернуть строку из массива, * не используется из-за неявного характера указателя каждой строки. Если используется *, то будет возвращен первый элемент строки.
Указатель на функцию, возвращающую указатель
Следующая программа показывает, как кодируется указатель на функцию, возвращающую указатель:
int * fn ( )
<
int num = 4 ;
int * inter = & num ;
return inter ;
>
int * ( * func ) ( ) = & fn ;
int val = * func ( ) ;
Объявление указателя на функцию, возвращающую указатель, аналогично объявлению указателя на обычную функцию, но перед ним стоит звездочка. Первый оператор в функции main () иллюстрирует это. Чтобы вызвать функцию с помощью указателя, поставьте перед ней *.
Заключение
Чтобы создать указатель на скаляр, сделайте что-нибудь вроде:
* имеет два значения: в объявлении указывает указатель; чтобы что-то вернуть, это значение указанного объекта.
Имя массива — это постоянный указатель на первый элемент массива.
Чтобы создать указатель на функцию, вы можете:
где fn () — функция, определенная в другом месте, а func — указатель.
Чтобы создать ссылку на функцию, вы можете:
где fn () — функция, определенная в другом месте, а refFunc — ссылка.
Когда функция возвращает указатель, возвращаемое значение должно быть получено указателем. Когда функция возвращает ссылку, возвращаемое значение должно быть получено по ссылке.
При передаче указателя на функцию параметр является объявлением, а аргумент — адресом указанного объекта. При передаче ссылки на функцию параметр является объявлением, а аргумент — ссылкой.
При передаче массива функции параметр — это объявление, а аргумент — это имя массива без []. Функция C ++ не возвращает массив.
Для указателя на указатель требуется два * вместо одного, где это необходимо.