Указатели
Адрес и указатель
Каждая переменная занимает в памяти несколько байтов и имеет адрес — номер первого байта. Указатель (pointer) — это переменная, которая хранит адрес другой переменной.
int x = 42;
int *p = &x; /* p хранит адрес x */
printf("x = %d\n", x);
printf("адрес x = %p\n", (void *)p);
printf("значение по адресу = %d\n", *p); /* 42 */
Аналогия. Переменная — дом, в котором живёт значение. Указатель — листок с адресом этого дома. Операция
&берёт адрес, операция*позволяет прийти по адресу и работать со значением.
Тип указателя важен: int * указывает на int, double * — на double. Компилятор должен
знать, сколько байтов занимает объект, на который смотрит указатель.
Две основные операции
| Операция | Запись | Что делает |
|---|---|---|
| Взять адрес | &x | возвращает указатель на x |
| Разыменовать | *p | даёт объект, на который указывает p |
int x = 10;
int *p = &x;
*p = 20; /* изменили x через указатель */
printf("%d\n", x); /* 20 */
*p — это не «умножение», а унарная операция разыменования. Результат *p является
полноценной переменной: её можно читать и присваивать ей новое значение.
Нулевой указатель
Указатель, который пока никуда не указывает, должен содержать NULL:
int *p = NULL;
if (p != NULL)
{
printf("%d\n", *p);
}
NULL нельзя разыменовывать — это аварийное завершение программы. Проверка на NULL
обязательна после функций, которые могут не найти объект или не выделить память
(malloc, calloc, realloc).
Указатели и функции
Обычные аргументы передаются в функцию по значению: функция получает копию. Поэтому изменить переменную вызывающего кода через обычный параметр нельзя. Если передать адрес, функция сможет изменить оригинал:
#include <stdio.h>
void swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
int main(void)
{
int x = 1, y = 2;
swap(&x, &y);
printf("%d %d\n", x, y); /* 2 1 */
return 0;
}
Указатель сам передаётся по значению, но значение указателя — это адрес. Поэтому через него доступен исходный объект. Так же можно «вернуть» из функции несколько результатов:
void minmax(const int *a, int n, int *mn, int *mx)
{
*mn = *mx = a[0];
for (int i = 1; i < n; i++)
{
if (a[i] < *mn)
{
*mn = a[i];
}
if (a[i] > *mx)
{
*mx = a[i];
}
}
}
Вызов: minmax(a, n, &min_value, &max_value);.
Указатели и массивы
Имя массива в выражениях превращается в указатель на первый элемент. Поэтому эти записи эквивалентны:
int a[5] = { 10, 20, 30, 40, 50 };
int *p = a; /* то же, что &a[0] */
printf("%d\n", a[2]); /* 30 */
printf("%d\n", *(a + 2)); /* 30 */
printf("%d\n", p[2]); /* 30 — к указателю тоже применима [] */
Важно: имя массива — не совсем обычный указатель. В выражении sizeof(a) получается размер
всего массива, а не размер указателя. Но при передаче в функцию массив «теряет» размер и
передаётся как указатель на первый элемент — поэтому количество элементов передают отдельным
параметром.
Обход массива через указатель:
int sum = 0;
for (int *p = a; p < a + 5; p++)
{
sum += *p;
}
Арифметика указателей
К указателю можно прибавлять и вычитать целое число. Сдвиг происходит на элементы, а не на байты:
int a[5] = { 10, 20, 30, 40, 50 };
int *p = a; /* указывает на a[0] */
p++; /* теперь указывает на a[1] */
printf("%d\n", *p); /* 20 */
Если int занимает 4 байта, то p + 1 сдвигает адрес на 4 байта. Для double — на 8 байтов.
Компилятор сам умножает шаг на sizeof(*p).
Арифметика указателей имеет смысл только внутри массива (или на один элемент за его концом). Сравнивать и вычитать указатели, которые смотрят в разные массивы, нельзя.
Указатель на указатель
Указатель — такая же переменная, поэтому можно взять адрес и у него:
int x = 10;
int *p = &x;
int **pp = &p; /* указатель на указатель на int */
printf("%d\n", **pp); /* 10 */
int ** пригодится, когда функция должна изменить сам указатель, например голову списка
или массив строк. Подробнее — в теме «Односвязные списки».
const и указатели
const помогает компилятору запретить случайную запись:
void print_array(const int *a, int n); /* функция только читает массив */
int x = 10;
const int *p = &x; /* нельзя сделать *p = 20 */
int * const q = &x; /* нельзя переставить q, но можно *q = 20 */
Для функций, которые только читают данные, ставьте const — это хороший стиль и защита от
ошибок.
Частые ошибки
| Ошибка | Что не так |
|---|---|
int *p; *p = 5; | указатель не инициализирован, запись по «мусорному» адресу |
printf("%d", p) вместо *p | печатается адрес, а не значение |
разыменование NULL | аварийное завершение программы |
p + 1 для указателя вне массива | неопределённое поведение |
путаница *p++ и (*p)++ | разные операции: сдвиг указателя и изменение значения |
sizeof(p) вместо sizeof(*p) | получается размер указателя, а не объекта |
Документация
Что запомнить
- Указатель хранит адрес;
&берёт адрес,*разыменовывает. NULL— указатель «в никуда»; разыменовывать его нельзя.- Через указатель функция может изменить аргумент вызывающего кода.
- Имя массива в выражении — адрес первого элемента;
a[i]равно*(a + i). - Арифметика указателей считается в элементах и допустима только внутри массива.
- Для чтения данных используйте
const, для изменения — обычный указатель.