Все определенные в программе данные, например, переменные, хранятся в памяти по определенному адресу. И указатели позволяют напрямую обращаться к этим адресам и благодаря этому манипулировать данными. Указатели представляют собой объекты, значением которых служат адреса других объектов (переменных, констант, указателей) или функций. Указатели - это неотъемлемый компонент для управления памятью в языке Си.
Для определения указателя надо указать тип объекта, на который указывает указатель, и символ звездочки *.
тип_данных* название_указателя;
Сначала идет тип данных, на который указывает указатель, и символ звездочки *. Затем имя указателя.
Например, определим указатель на объект типа int:
int *p;
Пока указатель не ссылается ни на какой объект. Теперь присвоим ему адрес переменной:
int main(void) { int x = 10; // определяем переменную int *p; // определяем указатель p = &x; // указатель получает адрес переменной return 0; }
Указатель хранит адрес объекта в памяти компьютера. И для получения адреса к переменной применяется операция &. Эта операция применяется только к таким объектам, которые хранятся в памяти компьютера, то есть к переменным и элементам массива.
Что важно, переменная x имеет тип int, и указатель, который указывает на ее адрес тоже имеет тип int. То есть должно быть соответствие по типу.
Какой именно адрес имеет переменная x? Для вывода значения указателя можно использовать специальный спецификатор %p:
#include <stdio.h> int main(void) { int x = 10; int *p; p = &x; printf("%p \n", p); // 0060FEA8 return 0; }
В моем случае машинный адрес переменной x - 0x0060FEA8. (Для адресов в памяти применяется шестнадцатеричная система.) Но в каждом отдельном случае адрес может быть иным. Фактически адрес представляет целочисленное значение, выраженное в шестнадцатеричном формате.
То есть в памяти компьютера есть адрес 0x0060FEA8, по которому располагается переменная x.
Так как переменная x представляет тип int, то на большинстве архитектур она будет занимать следующие 4 байта (на конкретных архитектурах размер памяти для типа int может отличаться). Таким образом, переменная типа int последовательно займет ячейки памяти с адресами 0x0060FEA8, 0x0060FEA9, 0x0060FEAA, 0x0060FEAB.
И указатель p будет ссылаться на адрес, по которому располагается переменная x, то есть на адрес 0x0060FEA8.
Стоит отметить, что при выводе адреса указателя функция printf()
ожидает, что указатель будет представлять void*
, то есть указатель на значение типа
void
. Поэтому некоторые компиляторы при некоторых настройках могут при компиляции отображать предупреждения. И чтобы было все канонически правильно, то
переданный указатель нужно преобразовать в указатель типа void *
:
printf("%p \n", (void *)p);
Но так как указатель хранит адрес, то мы можем по этому адресу получить хранящееся там значение, то есть значение переменной x. Для этого применяется операция * или операция разыменования (dereference operator). Результатом этой операции всегда является объект, на который указывает указатель. Применим данную операцию и получим значение переменной x:
#include <stdio.h> int main(void) { int x = 10; int *p; p = &x; printf("Address = %p \n", (void*) p); printf("x = %d \n", *p); return 0; }
Консольный вывод:
Address = 0060FEA8 x = 10
Используя полученное значение в результате операции разыменования мы можем присвоить его другой переменной:
int x = 10; int *p = &x; int y = *p; // присваиваем переменной y значение по адресу из указателя p printf("x = %d \n", y); // 10
Здесь присваиваем переменной y
значение по адресу из указателя p
, то есть значение переменной x
.
И также используя указатель, мы можем менять значение по адресу, который хранится в указателе:
int x = 10; int *p = &x; *p = 45; printf("x = %d \n", x); // 45
Так как по адресу, на который указывает указатель, располагается переменная x, то соответственно ее значение изменится.
Создадим еще несколько указателей:
#include <stdio.h> int main(void) { char c = 'N'; int d = 10; short s = 2; char *pc = &c; // получаем адрес переменной с типа char int *pd = &d; // получаем адрес переменной d типа int short *ps = &s; // получаем адрес переменной s типа short printf("Variable c: address=%p \t value=%c \n", (void*) pc, *pc); printf("Variable d: address=%p \t value=%d \n", (void*) pd, *pd); printf("Variable s: address=%p \t value=%hd \n", (void*) ps, *ps); return 0; }
В моем случае я получу следующий консольный вывод:
Variable c: address=0060FEA3 value=N Variable d: address=0060FE9C value=10 Variable s: address=0060FE9A value=2
По адресам можно увидеть, что переменные часто расположены в памяти рядом, но не обязательно в том порядке, в котором они определены в тексте программы: