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