Глиф Учебники

Урок 7. Указатели

Курс «C» · урок 7 из 10 · 4 минуты чтения

У каждой переменной есть место в памяти, а у этого места — номер, адрес. Указатель — переменная, в которой хранится адрес другой переменной. Через указатель можно прочитать и изменить ту переменную, на которую он указывает. Указатели — самая важная и самая непривычная часть C: на них держатся scanf, массивы, строки и динамическая память.

Операции & и *

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);
    return 0;
}
Вывод
x = 10
*p = 10
x = 25

После p = &x запись *p — это та же самая переменная x, только под другим именем. Присваивание *p = 25 изменило x.

int x = 10; int *p = &x; *p = 25; p int * 0x7ffd5c3e1a4c в p лежит адрес x x int 10 25 *p &x = 0x7ffd5c3e1a4c адрес для примера: при каждом запуске он свой
В указателе p хранится адрес переменной x, то есть &x; запись *p обращается к тому, что лежит по этому адресу, поэтому *p = 25 меняет x с 10 на 25.

Сам адрес выводится форматом %p; у указателя для этого нужно приведение к void * (указатель без типа, «на что угодно»):

C
#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

Из прошлого урока: функция получает копии значений и не может изменить переменные вызывающего. Если передать адреса, может:

C
#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
main int x = 3, y = 7; x 3 7 y 7 3 swap(&x, &y); void swap(int *a, int *b) a b адрес x адрес y int t = *a; t = 3 *a = *b; x = 7 *b = t; y = 3 t 3 своя переменная
swap(&x, &y) получает адреса x и y в указатели a и b; через *a и *b функция меняет сами переменные main: t запоминает 3, x становится 7, y — 3.

Именно поэтому в scanf("%d", &n) стоит &: scanf получает адрес n и записывает число прямо туда.

Так же функция может «вернуть» несколько значений сразу — через параметры-указатели:

C
#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]. Поэтому массив можно положить в указатель и обращаться через него к элементам:

C
#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 — одно и то же.

int *p = a; шаг +1 — следующий элемент, +4 байта p p + 2 p + 3 a[0]a[1]a[2]a[3] 10203040 *(p + 2) = 30 p[2] = 30 *(p + 3) = 40 после p++ p указывает на 20
Указатель p = a смотрит на первый элемент массива; p + 2 и p + 3 — адреса элементов на две и три позиции дальше, *(p + 2) даёт 30, а после p++ указатель сдвигается на 20.

Разница всё же есть: указатель — переменная, его можно сдвинуть (p++), а имя массива — нет. И sizeof у массива даёт размер всего массива, а у указателя — 8 байт.

NULL — «никуда»

Указатель, который ни на что не указывает, принято делать равным NULL. Многие функции возвращают NULL как знак неудачи: fopen — если файл не открылся, malloc — если не хватило памяти. Перед использованием такой указатель проверяют:

C
#include <stdio.h>

int main(void) {
    int *p = NULL;
    if (p == NULL) {
        printf("Указатель пустой\n");
    } else {
        printf("Значение: %d\n", *p);
    }
    return 0;
}
Вывод
Указатель пустой

Частые ошибки

Разыменование указателя, который никуда не указывает, — неопределённое поведение. Обычно программа падает с segmentation fault, но может и испортить чужие данные без всякого сообщения.

Коротко

Проверьте себя

1. Что выведет программа?
C
#include <stdio.h>

int main(void) {
    int a = 5;
    int *p = &a;
    *p = *p * 2;
    printf("%d\n", a);
    return 0;
}
2. Почему функция void swap(int a, int b) не меняет местами переменные вызывающего?
3. Чему равно *(a + 2), если int a[] = {4, 8, 15, 16};?
4. Что вернёт fopen, если файл не удалось открыть?