Переменная, которая хранит не значение, а место, где оно лежит
const int * и int *constNULL, висячийУ каждой переменной есть адрес — номер первого её байта в памяти (раздел 3, пункт 01). Операция & даёт адрес. Указатель — переменная, в которой хранится адрес. Тип указателя говорит, значение какого типа лежит по адресу: int *p — «p — указатель на int». Операция * перед указателем — разыменование: обращение к значению по адресу.
/* Адрес, указатель, разыменование */
#include <stdio.h>
int main(void)
{
int count = 25;
int *p = &count; /* p хранит адрес count */
printf("count = %d, *p = %d\n", count, *p);
printf("&count = %p, p = %p\n", (void *)&count, (void *)p);
*p = 40; /* записать 40 туда, куда указывает p */
printf("count = %d\n", count);
return 0;
}
count = 25, *p = 25
&count = 000000000061FE14, p = 000000000061FE14 <- адрес разный при каждом запуске и на разных ОС
count = 40
p count
┌──────────────┐ ┌──────────┐
│ 0x61FE14 ───┼──────► │ 40 │ адрес 0x61FE14
└──────────────┘ └──────────┘
8 байт (адрес) 4 байта (int)
%p выводит адрес в шестнадцатеричном виде; стандарт требует передать его как void * — отсюда (void *).int *p) и в выражении (*p = 40) — разные вещи: в объявлении она говорит «это указатель», в выражении — «взять значение по адресу».int *p, q; объявляет указатель p и обычный int q: звёздочка относится к имени, а не к типу. Правило курса — один указатель на объявление.В разделе 7 swap не работал: функция меняла копии. Если передать адреса, функция сможет записать значения по ним — в переменные вызывающего:
/* Обмен через указатели */
#include <stdio.h>
void swap(int *a, int *b)
{
int t = *a;
*a = *b;
*b = t;
}
int main(void)
{
int x = 1, y = 2;
swap(&x, &y);
printf("x = %d, y = %d\n", x, y); /* x = 2, y = 1 */
return 0;
}
Параметры по-прежнему передаются по значению — копируются адреса. Но копия адреса указывает туда же, куда оригинал. Так работает scanf("%d", &n): ему нужен адрес, чтобы записать прочитанное число в n.
Несколько результатов. return возвращает одно значение. Остальные функция записывает по указателям-параметрам («выходным параметрам»). Наименьшее и наибольшее элементов массива за один проход:
/* Наименьший и наибольший — по указателям; n >= 1 */
void min_max(const 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 lo, hi; min_max(marks, n, &lo, &hi); */
Частый образец: функция возвращает признак успеха, а результат записывает по указателю. Так устроен scanf, и так удобно писать свои функции чтения:
/* Прочитать целое в *value; true — прочитано */
bool read_value(int *value)
{
return scanf("%d", value) == 1; /* value уже адрес — & не нужен */
}
NULL и указатели, которые никуда не указываютNULL (из <stdio.h>, <stdlib.h>) — особое значение указателя: «не указывает ни на что». Им помечают отсутствие объекта: функция поиска может вернуть NULL, если не нашла. Разыменование NULL — неопределённое поведение, на практике программа аварийно завершается (в Linux — Segmentation fault, в Windows — молча, с кодом завершения −1073741819). Поэтому указатель, который может быть NULL, проверяют перед использованием: if (p != NULL) { … }.
| Ошибка | Пример | Что происходит | Предупреждение gcc |
|---|---|---|---|
| указатель без значения | int *p; *p = 5; | запись по случайному адресу — порча памяти или крах | 'p' is used uninitialized, не всегда |
разыменование NULL | int *p = NULL; *p = 5; | крах | обычно нет |
| адрес локальной переменной из функции | int *f(void) { int x = 5; return &x; } | после выхода из функции её кадр снят со стека (раздел 7, пункт 06), указатель висячий | function returns address of local variable [-Wreturn-local-addr] |
| указатель не того типа | double d; int *p = &d; | чтение double как int — бессмыслица | initialization of 'int *' from incompatible pointer type 'double *' |
NULL. Функция, которая может получить NULL, проверяет это. Адрес локальной переменной не возвращается из функции.Имя массива в выражении превращается в адрес его первого элемента: a — то же, что &a[0]. К указателю можно прибавить целое: p + 1 — адрес следующего элемента, а не следующего байта: адрес увеличивается на sizeof(*p). Отсюда главное равенство:
a[i] ≡ *(a + i) &a[i] ≡ a + i
Индексация — это и есть адресная арифметика, записанная короче. Обход массива указателем:
/* Адресная арифметика */
#include <stdio.h>
int main(void)
{
int a[5] = {10, 20, 30, 40, 50};
int *p = a; /* &a[0] */
printf("%d %d %d\n", *p, *(p + 2), p[4]); /* 10 30 50 */
int *last = &a[4];
printf("elements between: %td\n", last - p); /* 4 */
printf("bytes between: %td\n", (char *)last - (char *)p); /* 16 */
long long sum = 0;
for (const int *q = a; q < a + 5; q++) { /* от первого до «за последним» */
sum += *q;
}
printf("sum %lld\n", sum); /* 150 */
return 0;
}
10 30 50
elements between: 4
bytes between: 16
sum 150
ptrdiff_t, формат %td). Чтобы увидеть байты, указатели приводят к char * — шаг char — 1 байт.<, > имеет смысл для указателей в пределах одного массива. Указатель «за последним элементом» (a + 5) создавать и сравнивать можно, разыменовывать — нельзя.int a[] функции — на самом деле int *a: в функцию приходит адрес первого элемента (раздел 8, пункт 02). Записи void f(int a[]) и void f(int *a) равнозначны.sizeof a — 20, sizeof p — 8; массиву нельзя присвоить (a = p — ошибка), указателю — можно. А *p++ — это *(p++): взять значение и сдвинуть указатель, а не увеличить значение; для значения — (*p)++.const и указателиconst может относиться к значению по адресу или к самому указателю — зависит от того, по какую сторону звёздочки он стоит:
| Объявление | Менять *p | Менять p | Где встречается |
|---|---|---|---|
const int *p | нельзя | можно | параметр «только чтение»: const int a[] — это он; строки в printf, strlen |
int *const p | можно | нельзя | указатель, закреплённый за одним объектом |
const int *const p | нельзя | нельзя | редко |
Читается справа налево: const int *p — «p — указатель на int, который константа». Передать int * туда, где ждут const int *, можно без приведения: функция обещает только читать. Обратно — нельзя: gcc предупреждает discards 'const' qualifier from pointer target type.
Параметр const char *prompt функции read_int из раздела 7 — именно это: указатель на символы строки, которые функция не меняет. Строки — следующий раздел.
Указатели нужны там, где без них не обойтись:
swap, min_max, scanf;malloc, которая возвращает указатель (раздел 17);qsort (раздел 15).В C++ (раздел 20) для первого случая появятся ссылки, для третьего — std::vector, но устроены они на тех же адресах.
&x — адрес x; int *p — указатель на int; *p — значение по адресуNULL проверяют до разыменования; адрес локальной переменной не возвращаютa[i] — это *(a + i); p + 1 — следующий элемент, а не байтconst int *p — значение только для чтения; int *const p — сам указатель неизмененchar *; раздела 13 — указатель на структуру и FILE *; раздела 15 — указатель на функцию; раздела 17 — динамическая память и связные структуры; раздела 20 — ссылки C++.