Переменная — это участок памяти, к которому мы обращаемся по идентификатору, то есть имени переменной. У этой области есть уникальный адрес, выраженный числовым значением, понятным для компьютера. Адрес можно сохранить в специальной переменной, именуемой указателем.
Когда изменяем значение обычной переменной, фактически удаляем старые данные из соответствующей памяти и размещаем новые. При изменении значения переменной-указателя, обращаемся к другому участку памяти, поскольку меняем адрес.
Указатели тесно связаны с динамическими типами данных. Во время компиляции программы выделяются участки памяти под переменные, их размер неизменен, но содержимое может изменяться. Именно указатели позволяют управлять новыми участками памяти на этапе выполнения программы. Динамические типы данных будут рассмотрены позже.
Перед объяснением объявления и определения переменных-указателей, разберемся с тем, что такое адрес переменной и как его получить.
int i = 0;
printf ("i=%d, &i=%p \n", i, &i);
Результат этого кода выводит приблизительно следующее (ваше шестнадцатеричное значение адреса будет отличаться):
i=0, &i=0x7fffa40c5fac
Символ амперсанда (&) перед переменной извлекает ее адрес. Формат %p
используется для вывода адреса на экран. Адреса
обычных переменных неизменны во время работы программы, в чем можно убедиться:
int a = 6;
float b = 10.11;
char c = 'k';
printf("%d - %p, %.2f - %p, %c - %p\n", a,&a, b,&b, c,&c);
a = 2; b = 50.99; c = 'z';
printf("%d - %p, %.2f - %p, %c - %p\n", a,&a, b,&b, c,&c);
Результат:
span>6 - 0x7fff8e1d38e4, 10.11 - 0x7fff8e1d38e8, k - 0x7fff8e1d38ef
2 - 0x7fff8e1d38e4, 50.99 - 0x7fff8e1d38e8, z - 0x7fff8e1d38ef
Изменение значений переменных не влияет на их адреса памяти.
Зная адрес, можно извлечь значение, находящееся там, используя * перед адресом:
int a = 8;
printf("%d \n", *&a);
На экране будет показано число 8.
Запись &a
не всегда удобна. Поэтому введены указатели, которые сохраняют адрес области памяти.
Указатели в языке C являются типизированными: при их объявлении указывают тип. Позже мы изучим, как с указателями выполняются арифметические операции, и насколько важно их точное определение. Чтобы объявить указатель, нужно поставить * перед его именем. Пример:
int *pi;
float *pf;
Обратите внимание, * при объявлении обозначает указатель; в выражениях — извлечение значения по адресу указателя. Рассмотрим код:
int x = 1, y, z = 3;
int *p, *q;
p = &x;
printf("%d\n", *p); // 1
y = *p;
printf("%d\n", y); // 1
*p = 0;
printf("%d %d\n", x, y); // 0 1
q = &z;
printf("%d\n", *q); // 3
p = q;
*p = 4;
printf("%d\n", z); // 4
printf("%p %p\n", p, q); // p == q
Комментарии показывают текущие состояния памяти. Подробно описываем происходящее:
- Под пять переменных выделяется память: три для
int
переменных и два для указателей. В x и z записаны 1 и 3 соответственно. - Указатель p получает адрес x. Извлекая значение (
*p
), получаем 1. - y присваивается значение из ячейки, на которую указывает p. Обе области памяти (x и y) содержат единицы.
- По адресу p записывается 0, меняя x. Поскольку p указывает на x, y не затрагивается.
- Указатель q получает адрес z. Извлекая значение (
*q
), получаем 3. - p получает значение из q, что приводит к ссылке p и q на z.
- По адресу p пишем 4, меняя z. P, как указатель на z, изменяет и его значение.
- Убедимся, что p и q указывает на тот же участок памяти.
Для хранения адреса указателя также должна быть выделена память, что можно определить функцией sizeof()
:
int *pi;
float *pf;
printf("%lu\n", sizeof(pi));
printf("%lu\n", sizeof(pf));
Для всех указателей выделяется одинаковое количество памяти. Это связано с тем, что размер адреса обусловлен системой, а не типом данных. Зачем же указывать тип? Тип данных определяет размеры и диапазоны памяти, на которые ссылается указатель, а также вычисления между областью памяти и указателем.
Если указатель объявлен без определения, он может указывать на любой адрес с непредсказуемым значением:
int *pa, *pb;
float *pc;
printf(" %p %p %p\n", pa, pc, pb);
printf(" %d %f\n", *pa, *pc); // может возникнуть ошибка
Результат (в Ubuntu):
0x400410 0x7fff5b729580 (nil)
-1991643855 0.000000
Использование неопределенных указателей может привести к серьезным ошибкам. Чтобы этого избежать, присваивают значение NULL, говорящее о том, что указатель ни на что не указывает. Пока ему не установят конкретное значение, его использовать нельзя:
int a = 5, b = 7;
float c = 6.98;
int *pa, *pb;
float *pc;
pa = pb = NULL;
pc = NULL;
printf(" %15p %15p %15p\n", pa, pc, pb);
// printf(" %15d %15f %15d\n", *pa, *pc, *pb); // Error
pa = &a;
pb = &b;
pc = &c;
printf(" %15p %15p %15p\n", pa, pc, pb);
printf(" %15d %15f %15d\n", *pa, *pc, *pb);
Результат (в Ubuntu):
(nil) (nil) (nil)
0x7fffeabf4e44 0x7fffeabf4e4c 0x7fffeabf4e48
5 6.980000 7
Если извлечь значение по неназначенному указателю, возникает "ошибка сегментирования".
На занятии важными концепциями являются: адрес переменной (&var
), указатель определенной переменной
(type *p_var; p_var = &var
), получение значения по адресному указателю
(*p_var
). Если это вызывает некоторое недоумение, не переживайте. Значимость указателей раскроется позже с изучением новых тем.
Продемонстрируйте все примеры из данного урока на практике и придумайте собственные примеры использования указателей.