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

Как вывести указатель в си

  • автор:

Указатели в Си

Указатель – переменная, в которой хранится адрес какого-либо объекта в памяти компьютера, например, другой переменной. Мы уже сталкивались раньше с адресами переменных, когда изучали функцию scanf .

Итак, пойдём по порядку. Объявление указателя.

Объявление указателя отличается от объявления переменной только добавлением символа * после названия типа. Примеры:

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

Для вывода значения указателя на экран нужно в функции printf использовать спецификатор %p . Пример:

Используя адрес переменной, который хранится в указателе, можно изменять значения этой переменной. Для этого используется операция разыменования * . Вот посмотрите на пример:

Доступ к переменной через указатель

Рис.1 Доступ к переменной через указатель

Итого, * применительно к указателям используется в двух случаях:

  • при объявлении указателя, чтобы показать, что это указатель;
  • если мы хотим обратиться к переменной, на которую указывает указатель.

Есть еще, так называемый, нулевой указатель NULL . Нулевой указатель не ссылается никуда. Он используется, чтобы обнулять указатели. Посмотрите на пример.

Указатели

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

Определение указателя

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

Сначала идет тип данных, на который указывает указатель, и символ звездочки *. Затем имя указателя.

Например, определим указатель на объект типа int:

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

Получение адреса данных

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

Что важно, переменная x имеет тип int, и указатель, который указывает на ее адрес тоже имеет тип int. То есть должно быть соответствие по типу.

Какой именно адрес имеет переменная x? Для вывода значения указателя можно использовать специальный спецификатор %p :

В моем случае машинный адрес переменной x — 0x0060FEA8. (Для адресов в памяти применяется шестнадцатеричная система.) Но в каждом отдельном случае адрес может быть иным. Фактически адрес представляет целочисленное значение, выраженное в шестнадцатеричном формате.

То есть в памяти компьютера есть адрес 0x0060FEA8, по которому располагается переменная x.

Указатели в языке Си

Так как переменная x представляет тип int , то на большинстве архитектур она будет занимать следующие 4 байта (на конкретных архитектурах размер памяти для типа int может отличаться). Таким образом, переменная типа int последовательно займет ячейки памяти с адресами 0x0060FEA8, 0x0060FEA9, 0x0060FEAA, 0x0060FEAB.

Указатели в Си

И указатель p будет ссылаться на адрес, по которому располагается переменная x, то есть на адрес 0x0060FEA8.

Стоит отметить, что при выводе адреса указателя функция printf() ожидает, что указатель будет представлять void* , то есть указатель на значение типа void . Поэтому некоторые компиляторы при некоторых настройках могут при компиляции отображать предупреждения. И чтобы было все канонически правильно, то переданный указатель нужно преобразовать в указатель типа void * :

Получение значения по адресу

Но так как указатель хранит адрес, то мы можем по этому адресу получить хранящееся там значение, то есть значение переменной x. Для этого применяется операция * или операция разыменования (dereference operator). Результатом этой операции всегда является объект, на который указывает указатель. Применим данную операцию и получим значение переменной x:

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

Здесь присваиваем переменной y значение по адресу из указателя p , то есть значение переменной x .

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

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

Создадим еще несколько указателей:

В моем случае я получу следующий консольный вывод:

По адресам можно увидеть, что переменные часто расположены в памяти рядом, но не обязательно в том порядке, в котором они определены в тексте программы:

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

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

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

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