Раздел 10 — Указатели

Переменная, которая хранит не значение, а место, где оно лежит

Прогресс курса Раздел 10 из 28

Что вы освоите в этом разделе

3 академических часа теории, практической работы в разделе нет — указатели нужны в следующей же работе (№9, строки) и во всех дальнейших. Раздел заканчивается тестом на портале.
01

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

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

Указатель как параметр функции

В разделе 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 уже адрес — & не нужен */
}
03

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, не всегда
разыменование NULLint *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, проверяет это. Адрес локальной переменной не возвращается из функции.
04

Указатель и массив, адресная арифметика

Имя массива в выражении превращается в адрес его первого элемента: 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
Массив — не указатель. Имя массива превращается в адрес, но сам массив — 20 байт данных, а указатель — 8 байт адреса. Поэтому sizeof a — 20, sizeof p — 8; массиву нельзя присвоить (a = p — ошибка), указателю — можно. А *p++ — это *(p++): взять значение и сдвинуть указатель, а не увеличить значение; для значения — (*p)++.
05

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 — именно это: указатель на символы строки, которые функция не меняет. Строки — следующий раздел.

06

Зачем всё это

Указатели нужны там, где без них не обойтись:

В C++ (раздел 20) для первого случая появятся ссылки, для третьего — std::vector, но устроены они на тех же адресах.

Ключевые выводы раздела

Запомните главное

  • &x — адрес x; int *p — указатель на int; *p — значение по адресу
  • Функция меняет переменную вызывающего, получив её адрес; так возвращают несколько результатов
  • Указатель получает значение при объявлении; NULL проверяют до разыменования; адрес локальной переменной не возвращают
  • a[i] — это *(a + i); p + 1 — следующий элемент, а не байт
  • Разность указателей — число элементов между ними; указатель «за последним» можно сравнивать, но не разыменовывать
  • const int *p — значение только для чтения; int *const p — сам указатель неизменен
Связи раздела. Опирается на раздел 3 — адрес и размер типа; раздел 7 — передача по значению и стек вызовов; раздел 8 — массив в функции. Нужен для: раздела 11 — строки как char *; раздела 13 — указатель на структуру и FILE *; раздела 15 — указатель на функцию; раздела 17 — динамическая память и связные структуры; раздела 20 — ссылки C++.
Тест по разделу 10 — на учебном портале. Курс на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Раздел 9: Двумерные массивы Все разделы Раздел 11: Строки в C