Урок 7. Указатели
У каждой переменной есть место в памяти, а у этого места — номер, адрес. Указатель — переменная, в которой хранится адрес другой переменной. Через указатель можно прочитать и изменить ту переменную, на которую он указывает. Указатели — самая важная и самая непривычная часть C: на них держатся scanf, массивы, строки и динамическая память.
Операции & и *
&x— адрес переменнойx.int *p— объявление указателя наint: вpможно положить адрес целой переменной.*p— разыменование: «то, на что указываетp». Через*pзначение читают и записывают.
#include <stdio.h>
int main(void) {
int x = 10;
int *p = &x;
printf("x = %d\n", x);
printf("*p = %d\n", *p);
*p = 25;
printf("x = %d\n", x);
return 0;
}x = 10
*p = 10
x = 25После p = &x запись *p — это та же самая переменная x, только под другим именем. Присваивание *p = 25 изменило x.
Сам адрес выводится форматом %p; у указателя для этого нужно приведение к void * (указатель без типа, «на что угодно»):
#include <stdio.h>
int main(void) {
int x = 10;
int *p = &x;
printf("Адрес x: %p\n", (void *)p);
printf("Размер указателя: %zu байт\n", sizeof(p));
return 0;
}Адрес при каждом запуске свой (например, 0x7ffd5c3e1a4c), поэтому результата здесь нет. Размер указателя в 64-битной программе — 8 байт, независимо от того, на что он указывает.
Звёздочка в C работает в двух ролях, и это путает. В объявлении int *p она говорит «p — указатель». В выражении *p она значит «взять то, на что указывает p». Ещё одна ловушка: int *a, b; объявляет указатель a и обычную переменную b. Звёздочка относится к имени, а не к типу.
Зачем нужны указатели: функция swap
Из прошлого урока: функция получает копии значений и не может изменить переменные вызывающего. Если передать адреса, может:
#include <stdio.h>
void swap(int *a, int *b) {
int t = *a;
*a = *b;
*b = t;
}
int main(void) {
int x = 3, y = 7;
swap(&x, &y);
printf("x = %d, y = %d\n", x, y);
return 0;
}x = 7, y = 3Именно поэтому в scanf("%d", &n) стоит &: scanf получает адрес n и записывает число прямо туда.
Так же функция может «вернуть» несколько значений сразу — через параметры-указатели:
#include <stdio.h>
void min_max(int a[], int n, int *min, int *max) {
*min = a[0];
*max = a[0];
for (int i = 1; i < n; i++) {
if (a[i] < *min) *min = a[i];
if (a[i] > *max) *max = a[i];
}
}
int main(void) {
int a[] = {5, -2, 9, 0, 3};
int lo, hi;
min_max(a, 5, &lo, &hi);
printf("Наименьшее %d, наибольшее %d\n", lo, hi);
return 0;
}Наименьшее -2, наибольшее 9Указатели и массивы
Имя массива в выражениях превращается в адрес его первого элемента: a — то же, что &a[0]. Поэтому массив можно положить в указатель и обращаться через него к элементам:
#include <stdio.h>
int main(void) {
int a[4] = {10, 20, 30, 40};
int *p = a;
printf("%d %d\n", p[0], p[2]);
printf("%d %d\n", *p, *(p + 3));
p++;
printf("Теперь p указывает на %d\n", *p);
return 0;
}10 30
10 40
Теперь p указывает на 20Это арифметика указателей: p + 3 — адрес элемента на три позиции дальше (компилятор сам учитывает размер int), а p[i] — по определению то же, что *(p + i). Отсюда и то, что функции, принимающие массив, на самом деле получают указатель: параметры int a[] и int *a — одно и то же.
Разница всё же есть: указатель — переменная, его можно сдвинуть (p++), а имя массива — нет. И sizeof у массива даёт размер всего массива, а у указателя — 8 байт.
NULL — «никуда»
Указатель, который ни на что не указывает, принято делать равным NULL. Многие функции возвращают NULL как знак неудачи: fopen — если файл не открылся, malloc — если не хватило памяти. Перед использованием такой указатель проверяют:
#include <stdio.h>
int main(void) {
int *p = NULL;
if (p == NULL) {
printf("Указатель пустой\n");
} else {
printf("Значение: %d\n", *p);
}
return 0;
}Указатель пустойЧастые ошибки
Разыменование указателя, который никуда не указывает, — неопределённое поведение. Обычно программа падает с segmentation fault, но может и испортить чужие данные без всякого сообщения.
- Указатель без значения.
int *p; *p = 5;— вpмусор, запись идёт по случайному адресу. Сначала присвойте указателю адрес:p = &x;. - Разыменование NULL. Забыли проверить результат
fopenилиmalloc. - Адрес локальной переменной из функции. Функция вернула
&x, гдеx— её локальная переменная. После выхода из функцииxуже не существует, и указатель указывает на освобождённое место (такой указатель называют висячим). gcc предупреждает:function returns address of local variable. - Путаница
pи*p.p = 5меняет сам адрес (gcc ругается на это), а*p = 5— значение по адресу.
Коротко
&x— адрес переменной,int *p— указатель наint,*p— значение по адресу.- Через указатель функция может изменить переменную вызывающего:
swap(&x, &y). - Имя массива — адрес первого элемента;
p[i]то же, что*(p + i). NULL— пустой указатель; результатыfopenиmallocпроверяют наNULL.- Указатель без значения и висячий указатель — частые причины
segmentation fault.