Перейти к содержанию
Educora
Средний18 мин6 / 8

Указатели

Получай адрес переменной в памяти, читай и меняй значение через указатель, разберись в арифметике указателей и их связи с массивами и напиши функцию swap.

Проверь себя
В этом уроке ты узнаешь
  • Получать адрес оператором & и читать или менять значение по указателю оператором *
  • Изменять переменные вызывающей стороны, передавая в функцию указатели
  • Объяснять арифметику указателей и то, почему arr[i] — это то же, что *(arr + i)
  • Понимать опасность NULL и неинициализированных указателей

Чтобы показать другу, где ты живёшь, ты не приносишь ему дом — ты просто называешь адрес: «Баку, улица Низами, 25». Тот, кто знает адрес, может найти дом, войти внутрь и даже что-то там изменить. Память компьютера устроена так же: каждая переменная живёт по определённому адресу. Переменная, которая хранит адрес, называется указателем. Указатели — самая мощная и самая пугающая тема C, но идея у них такая же простая, как этот адрес.

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

Определение
Указатель

Переменная, значением которой является адрес другой переменной в памяти. int *p; означает: «p хранит адрес значения типа int».

ЗаписьЗначение
int *p;объявляет указатель на int
&xадрес переменной x
p = &x;теперь p указывает на x
*pзначение по адресу из p (разыменование)
NULLуказатель, который никуда не указывает
C
#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);
    printf("Same address? %d\n", p == &x);
    return 0;
}
Ожидаемый результат
x = 10
*p = 10
x = 25
Same address? 1
*p = 25; изменил x, даже не называя её, потому что p хранит её адрес.

Сам адрес можно напечатать спецификатором %p. Обычно он выглядит как шестнадцатеричное число вроде 0x7ffd… и может меняться при каждом запуске программы:

C
int x = 10;
int *p = &x;
printf("Address of x: %p\n", (void *) &x);
printf("Value of p:   %p\n", (void *) p);

Передача указателей в функции: swap

Вспомни: в C функция получает копии аргументов, поэтому через обычные параметры поменять местами значения двух переменных невозможно. Решение — передать функции адреса переменных. По адресам функция добирается до исходных переменных. Именно поэтому scanf("%d", &age) требует &.

C
#include <stdio.h>

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main(void) {
    int x = 3, y = 8;
    printf("Before: x = %d, y = %d\n", x, y);
    swap(&x, &y);
    printf("After:  x = %d, y = %d\n", x, y);
    return 0;
}
Ожидаемый результат
Before: x = 3, y = 8
After:  x = 8, y = 3

Указатели позволяют функции «вернуть» и несколько результатов: return возвращает лишь одно значение, но функция может записать результаты по переданным ей адресам. Функция ниже находит самую низкую и самую высокую из нескольких температур, измеренных в Баку:

C
#include <stdio.h>

void min_max(const int *arr, int n, int *min, int *max) {
    *min = arr[0];
    *max = arr[0];
    for (int i = 1; i < n; i++) {
        if (arr[i] < *min) *min = arr[i];
        if (arr[i] > *max) *max = arr[i];
    }
}

int main(void) {
    int temps[5] = {18, 25, 21, 30, 16};
    int low, high;
    min_max(temps, 5, &low, &high);
    printf("Min: %d, max: %d\n", low, high);
    return 0;
}
Ожидаемый результат
Min: 16, max: 30

Арифметика указателей и массивы

В выражениях имя массива превращается в адрес его первого элемента, поэтому можно написать int *p = arr;. Если прибавить к указателю 1, он сдвинется не на 1 байт, а на один элемент: для int * обычно на 4 байта. Значит, *(p + 2) — третий элемент, а запись arr[i] — просто сокращение для *(arr + i). При передаче массива в функцию на самом деле передаётся указатель на первый элемент — вот почему функция может менять исходный массив и почему приём с sizeof не работает с параметром.

C
#include <stdio.h>

int main(void) {
    int arr[4] = {10, 20, 30, 40};
    int *p = arr;
    printf("%d\n", *p);
    printf("%d\n", *(p + 2));
    p++;
    printf("%d\n", *p);
    printf("%d\n", arr[3] == *(arr + 3));
    int sum = 0;
    for (int *q = arr; q < arr + 4; q++) {
        sum += *q;
    }
    printf("Sum: %d\n", sum);
    return 0;
}
Ожидаемый результат
10
30
20
1
Sum: 100
p++ сдвинул указатель на следующий элемент. Последний цикл перебирает массив указателем вместо индекса.

Опасные указатели

  • Неинициализированный указатель хранит случайный адрес. Запись по нему может испортить другие данные. Всегда задавай указателю адрес переменной или NULL.
  • **Разыменование указателя NULL** (*p, когда p равен NULL) обычно сразу приводит к аварийному завершению. Проверяй перед использованием: if (p != NULL).
  • Висячий указатель указывает на переменную, которой уже нет. Например, если функция возвращает адрес своей локальной переменной, эта переменная исчезает, как только функция завершается.
C
int *make_number(void) {
    int local = 5;
    return &local;
}
Пример ОШИБКИ: local исчезает, когда функция завершается, и возвращённый адрес указывает «в никуда». Результат непредсказуем.

Главное

  • Указатель хранит адрес: int *p = &x; — p содержит адрес x.
  • & даёт адрес, * читает или меняет значение по этому адресу.
  • Функция, получившая указатели, может менять переменные вызывающей стороны (swap, scanf).
  • p + 1 сдвигает на один элемент; arr[i] означает *(arr + i).
  • Всегда инициализируй указатели, проверяй на NULL и не возвращай адрес локальной переменной.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Чему равна x после int x = 4; int *p = &x; *p = 9;?