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

Какой тип имеет разыменованный указатель

  • автор:

3.1 Разыменование указателя и получение адреса

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

3.1.1 Операции разыменования указателя и получение адреса переменной

Разыменованием указателя называется конструкция, позволяющая получить доступ к значению переменной, на которую ссылается указатель (т. е. адрес которой хранит указатель). Это может потребоваться в случае, если необходимо изменить или прочитать значение переменной через указатель. Разыменование указателя осуществляется с использованием символа «*» («звёздочка») перед именем указателя.

Получением адреса переменной называется конструкция, позволяющая сформировать указатель, хранящий адрес этой переменной. Такая операция осуществляется с использованием символа «&» («амперсанд») перед именем переменной. После этого через сформированный указатель становится возможно изменять значение исходной переменной (через операцию разыменования). В некотором смысле описанные операции являются обратными по отношению друг к другу.

Примеры применения операция разыменования указателей и получения адреса переменных приведены в листинге 6.

Листинг 6. Примеры разыменования указателей и получения адреса

// Определяем целочисленную переменную со значением 200 unsigned int value = 200;

// Определение указателя на переменную типа unsigned int

// с одновременной инициализацией адресом переменной value. unsigned int *pointerToValue = &value;

// Теперь pointerToValue указывает на ячейку памяти,

// начиная с которой хранится переменная value.

// Создаём ещё одну переменную с одновременной инициализацией

// значением переменной, на которую указывает pointerToValue unsigned int value2 = *pointerToValue; // Здесь value2 будет равно 200

// Используем разыменование для изменения значения переменной value *pointerToValue = 100;

// Теперь value = 100, а value2 по прежнему 200.

// Возможно поменять значение указателя (сделать так, чтобы он

// ссылался на другую переменную)

// Теперь (*pointerToValue) равно 200

3.1.2 Инициализация и присваивание указателей

Одним из способов изменения значения указателя является присвоение ему адреса некоторой переменной. Также часто применяется инициализация (или присваивание) специальным макросом NULL, что делает указатель нулевым. Нулевой указатель – это указатель на адрес памяти 0. Это специальное значение, показывающее, что указатель не указывает ни на какую переменную.

Разыменовывать нулевой указатель нельзя – поведение программы в таком случае непредсказуемо.

3.1.3 Указатели и область действия (время существования) переменных

При выполнении операции получения адреса переменной следует

учитывать, как долго эта переменная существует в памяти. Особую осторожность следует проявлять при получении адреса локальных переменных. Такие переменные обычно существуют на стеке и при выходе из функции уничтожаются, после чего на их месте могут оказаться другие данные. Следовательно, разыменование указателя на локальную переменную после выхода из функции, в которой она была определена, может привести к совершенно неожиданному поведению программы. Пример такой ситуации приведён в листинге 7.

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

Листинг 7. Пример ошибки при разыменовании указателя на несуществующую переменную ( не делайте так! )

// ВНИМАНИЕ! Программа в данном примере является некорректной. Это

// сделано намеренно для иллюстрации ошибки, связанной с

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

void function2 (int *ptr);

// Объявляем функцию, возвращающую указатель на переменную типа int int * function1 (void)

Кратко об указателях в Си: присваивание, разыменование и перемещение по массивам

Приветствую вас, дорогие читатели. В данной статье кратко описаны основные сведения об указателях в языке Си. Кроме основных операций с указателями (объявление, взятие адреса, разыменование) рассмотрены вопросы безопасности типов при работе с ними. К сожалению, в данной статье вы не найдёте информацию по операциям сравнений указателей. Однако, статья будет полезна новичкам, а также тем, кто работает с массивами. Все примеры в данной статье компилировались компилятором gcc (восьмой версии).

Введение

Указатель — переменная, которая хранит адрес сущностей (т.е. других переменных любого типа, будь то структура, или массив), и над которой возможно выполнять операцию разыменования (dereferencing). Адрес обычно выражен целым положительным числом. Диапазон адресов зависит от архитектуры компьютера. Указателю надо указать тип переменной, адрес которой он хранит, или же использовать ключевое слово void, для обозначения указателя, хранящего адрес чего-угодно (т.е. разрешён любой тип). Указатели объявляются как и обычные переменные, с той разницей, что имя типа переменной указателя имеет префикс, состоящий как минимум из одной звёздочки (*). Например:

Количество звёздочек лишь указывает на длину цепочек хранимых адресов. Поскольку указатель также является переменной и имеет адрес, то его адрес также можно хранить в другом указателе. В выше приведённом примере адрес переменной a сохраняется в переменной-указателе ptr. Адрес же самой переменной ptr сохраняется в другом указателе pptr. Чтобы получить адрес переменной, перед её именем надо поставить знак амперсанда (&). Наконец, чтобы выполнить обратную операцию, т.е. получить значение (содержимое) по адресу, хранимому в указателе, имя указателя предваряется звёздочкой, почти как при объявлении. Почти, потому что одной звёздочки достаточно чтобы «распаковать» указатель. Поскольку pptr указывает по адресу на значение, хранимое в ptr, то необходимо два раза применить операцию разыменования.

Указатели в предыдущем примере хранят адрес переменной определённого типа. В случае, когда применяются указатели типа void (любого типа), то прежде чем распаковать значение по адресу, необходимо выполнить приведение к типизированному указателю. Следующий пример является версией предыдущего, но с использованием указателя любого типа.

В данном примере адреса хранятся в указателе типа void. Перед получением значения по адресу, хранимым в pb, необходимо привести указатель pb к типу int*. Затем, воспользоваться стандартной операцией разыменования. Что касается указателя ppb, то он разыменовывается два раза. Первый раз до приведения к типу, для получения содержимого переменной pb, на которую он указывает. Второй раз — после приведения к типу int*.

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

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

Как было сказано выше, указатели хранят адреса. Естественно, что адреса могут указывать не только на ячейки данных переменных в вашей программе, но и на другие вещи: адрес стека процедур, адрес начала сегмента кода, адрес какой-то процедуры ядра ОС, адрес в куче и т. д. Логично, что не все адреса можно использовать напрямую в программе, поскольку некоторые из них указывают на те участки памяти, которые нельзя изменять (доступ для чтения), или которые нельзя затирать. В случае, при обращении к участку, доступному только для чтения, при попытке изменить значение получим ошибку Segmentation Fault (SF).

Кроме того, в языке Си определён макрос с именем NULL, для обозначения указателя с нулевым адресом. Данный адрес обычно используется операционной системой для сигнала об ошибке при работе с памятью. При попытке что либо читать по этому адресу, программа может получить неопределённое поведение. Поэтому ни в коем случае не пытайтесь извлечь значение по пустому указателю.

И ещё, указатели могут указывать на один и тот же объект. Например:

Этот простой пример показывает, что через адреса можно менять содержимое простых переменных, а также остальных указателей, ссылающихся на тоже самое. Таким образом, указатель p2 как бы является псевдонимом (alias) для p1.

Передача параметров через указатели.

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

Здесь переменные а и b меняются своими значениями друг с другом (при условии, что параметры содержат не нулевой адрес). Отметим ещё раз, что мы можем изменить содержимое, указываемое по параметру-указателю методов. И, конечно, мы можем стереть данный адрес, присвоив параметру новое значение.

Проверка типов и массивы

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

Конечно, компилятор gcc и без -Wall заметит недопустимую операцию в 7 строке кода. Флаг -Wall покажет все предупреждения компилятора. Главный флаг -Werror не позволит компилировать код, если есть предупреждения.

Что же касается массивов, то для массива не нужно предварять имя переменной амперсандом, поскольку компилятор автоматически при присваивании адреса массива присвоит адрес первого его элемента в указатель. Для многомерных массивов потребуются указатели на массивы, а не массивы указателей. Первые имеют форму объявления вида int (*arr)[] , а вторые вида int *arr[] . В квадратных скобках обязательно нужно указать размер массива. Для трёхмерных массивов потребуется уже две пары скобок, например int (*arr)[2][2] . Для четырёхмерных — три и так далее.

В выше приведённом коде даны примеры для работы с массивами (одномерными и двумерными). В квадратных скобках указывается размер последнего измерения. Важно помнить, что первое разыменование приводит вас ко всему массиву (т. е. к типу int * ). А второе разыменование распаковывает элемент данного массива. В случае одномерного массива, у нас всего одна ячейка, и указатель ссылается на неё. В случае двумерного массива, у нас две ячейки — массивы, а указатель ссылается на первую. Для перемещения на второй массив, достаточно прибавить единицу к адресу, хранимому в переменной mp, например, так mp + 1 . Чтобы получить первый элемент второго массива, надо два раза распаковать указатель с соответствующим адресом массива, т.е. **(mp + 1) .

Постоянные (const) и указатели.

Напомним, чтобы сделать переменную с постоянным, фиксированным значением, надо добавить ключевое слово const перед её именем (до имени типа или после). Например:

Для объявления указателя на постоянное значение, ключевое слово const должно быть ПЕРЕД звёздочкой.

В примере выше была создана переменная-указатель, ссылающееся на постоянное значение. Слово const перед звёздочкой указывает, что нельзя менять содержимое напрямую (путём разыменования, обращения к ячейке). Но сама переменная указатель постоянной не является. А значит, ей можно присвоить новый адрес. Например, адрес следующей ячейки в массиве.

Чтобы запретить менять адрес (значение переменной) указателя, надо добавить слово const ПОСЛЕ звёздочки. Кроме того, можно добавить ключевые слова const перед и после ‘*’ , чтобы сделать переменную фиксированной ещё сильнее, например так:

Указатели

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

Определение

У казатель – это переменная, которая хранит адрес области памяти. Указатель, как и переменная, имеет тип. Синтаксис объявления указателей

Например
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 (Все операции выполняются в шестнадцатиричном формате). Пусть мы создали указатель на начало массива. После этого мы можем «двигаться» по этому массиву, получая доступ до отдельных элементов.

Заметьте, каким образом мы получили адрес первого элемента массива

Массив, по сути, сам является указателем, поэтому не нужно использовать оператор &. Мы можем переписать пример по-другому

Получить адрес первого элемента и относительно него двигаться по массиву.
Кроме операторов + и — указатели поддерживают операции сравнения. Если у нас есть два указателя a и b, то a > b, если адрес, который хранит a, больше адреса, который хранит b.

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

Указатель на указатель

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

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

Указатели и приведение типов

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

В этом примере мы пользуемся тем, что размер типа int равен 4 байта, а char 1 байт. За счёт этого, получив адрес первого байта, можно пройти по остальным байтам числа и вывести их содержимое.

NULL pointer — нулевой указатель

У казатель до инициализации хранит мусор, как и любая другая переменная. Но в то же время, этот «мусор» вполне может оказаться валидным адресом. Пусть, к примеру, у нас есть указатель. Каким образом узнать, инициализирован он или нет? В общем случае никак. Для решения этой проблемы был введён макрос NULL библиотеки stdlib.
Принято при определении указателя, если он не инициализируется конкретным значением, делать его равным NULL.

По стандарту гарантировано, что в этом случае указатель равен NULL, и равен нулю, и может быть использован как булево значение false. Хотя в зависимости от реализации NULL может и не быть равным 0 (в смысле, не равен нулю в побитовом представлении, как например, int или float).
Это значит, что в данном случае

вполне корректная операция, а в случае

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

Примеры

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

2. Когда мы сортируем элементы часто приходится их перемещать. Если объект занимает много места, то операция обмена местами двух элементов будет дорогостоящей. Вместо этого можно создать массив указателей на исходные элементы и отсортировать его. Так как размер указателей меньше, чем размер элементов целевого массива, то и сортировка будет происходить быстрее. Кроме того, массив не будет изменён, часто это важно.

3. Более интересный пример. Так как размер типа char всегда равен 1 байт, то с его помощью можно реализовать операцию swap – обмена местами содержимого двух переменных.

В этом примере можно поменять тип переменных a и b на double или любой другой (с соответствующим изменением вывода и вызова sizeof), всё равно мы будет обменивать местами байты двух переменных.

Тип данных указателя в C++

Конструктор копирования в C++

Программирование и разработка

Указатель является переменной, которая хранит адрес памяти в объекте. Затем указатель просто «указывает» на объект. Тип объекта должен соответствовать типу указателя. Указатели широко используются как в C, так и в C++ для трех основных целей:

  • Чтобы разместить новые объекты в куче.
  • Для передачи функций другим функциям.
  • Для перебора элементов в массивах или других структурах данных.

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

Тип данных указателя

Указатель — это тип данных, и самая чистая его форма в C — это void *. Пустой * может передавать адрес памяти, который делает указатель, но его нельзя разыменовать.

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

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

Чтобы упростить жизнь, C предоставляет ряд производных типов указателей, таких как char *, int *, double * и т. д. Указатели имеют свой собственный размер независимо от типа данных, на которые они указывают.

  • char *x — указатель на char
  • int *x — указатель на целое число

Программа C для печати размера указателя

С

Вывод

Программа C++ для печати адреса указателя

С++

Вывод

Зачем нужно объявление типа данных указателя?

Тип данных указателя необходим в двух ситуациях:

  • Разыменование указателя.
  • Арифметика указателя.

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

Разыменование указателя

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

Ниже приведена программа C, демонстрирующая разыменование указателя int.

С

Арифметика указателя

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

Пример:

char takes 1 byte
char c = ‘a’;
char *ptr = &c;
ptr++;
Здесь ptr++ означает ptr = ptr + 1, поскольку char занимает 1 байт.
Это означает добавление 0×01 к адресу.

Точно так же для int это 4 байта, поэтому ptr++ в случае int будет добавлять 0×04 к адресу, хранящемуся в указателе.

Тип данных указателя — это переменная особого типа, предназначенная для хранения только адресов, а не значений (целое число, число с плавающей запятой, двойное число, символ и т. д.). Он знает, в скольких байтах хранятся данные. Когда мы увеличиваем указатель, мы увеличиваем указатель на размер типа данных, на который он указывает.
Рассмотрим следующий блок памяти:

Тип данных указателя — это переменная особого типа, предназначенная

Указатель занимает 8 байт памяти для хранения. В вышеуказанном блоке памяти-

  1. Если указатель ’ptr’ хранится в точке B (адрес между 1000000-10000099), тип указателя которого не объявлен (т. е. он будет сохранен в любом доступном месте) и изначально адрес указывает на переменную, которая находится между 2100 −2199, то доступ к переменной, на которую он указывает, займет много времени.
  2. Если указатель объявлен с типом данных, то он сохраняется в ближайшей доступной позиции для этого значения, например, сохраняется в точке A (адрес 2200-2299), которая является ближайшей к переменным типа «int», т.е. 4-байтовая строка, поэтому доступ также будет быть быстрее в сравнении.

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

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

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