Указатели

Адрес и указатель

Каждая переменная занимает в памяти несколько байтов и имеет адрес — номер первого байта. Указатель (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)получается размер указателя, а не объекта

Документация

Что запомнить

  1. Указатель хранит адрес; & берёт адрес, * разыменовывает.
  2. NULL — указатель «в никуда»; разыменовывать его нельзя.
  3. Через указатель функция может изменить аргумент вызывающего кода.
  4. Имя массива в выражении — адрес первого элемента; a[i] равно *(a + i).
  5. Арифметика указателей считается в элементах и допустима только внутри массива.
  6. Для чтения данных используйте const, для изменения — обычный указатель.

Подтверждение

—