Указатели и функции: изменяем аргументы

Урок показывает практическую силу указателей: как с их помощью функция меняет переменные вызывающего, как написать корректный swap и как «вернуть» несколько значений сразу.
Передавая в функцию указатель, вы даёте ей доступ к оригиналу. Это единственный способ в C изменить переменную вызывающего или вернуть из функции больше одного результата.

В разделе про функции мы видели, что аргументы копируются, поэтому наивный swap не работает. Указатели решают проблему. Передадим адреса — и функция поменяет оригиналы местами:

void swap(int *a, int *b) {
    int tmp = *a;    // запомнили значение по адресу a
    *a = *b;         // в a положили значение из b
    *b = tmp;        // в b положили запомненное
}

int main(void) {
    int x = 1, y = 2;
    swap(&x, &y);             // передаём АДРЕСА
    printf("%d %d\n", x, y);  // 2 1 — оригиналы поменялись!
    return 0;
}

Функция получила не копии значений, а адреса. Через разыменование *a и *b она работает прямо с оригинальными x и y.

Как работает под капотом

Сравним наивный и правильный swap по тому, что происходит в памяти:

Наивный swap(int a, int b):       Правильный swap(int *a, int *b):
  копии 1 и 2 внутри                a = &x (адрес x), b = &y
    a:1  b:2                          a ----> [x:1]
  меняем местами КОПИИ               b ----> [y:2]
    a:2  b:1                        меняем по адресам:
  оригиналы x,y не тронуты            *a, *b  -->  [x:2] [y:1]
    x:1  y:2  (не сработало)        оригиналы изменены! x:2 y:1

Тот же приём решает важную задачу: функция в C возвращает только одно значение через return. Но если нужно вернуть несколько результатов, их «отдают» через указатели-аргументы:

// Делим a на b: частное и остаток — оба результата через указатели
void divmod(int a, int b, int *quot, int *rem) {
    *quot = a / b;
    *rem  = a % b;
}

int main(void) {
    int q, r;
    divmod(17, 5, &q, &r);
    printf("17 / 5 = %d, остаток %d\n", q, r);  // 3, остаток 2
    return 0;
}

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

  • Забыли & при вызове. swap(x, y) вместо swap(&x, &y) передаст значения как адреса — крах или мусор.
  • Забыли * внутри функции. a = b; вместо *a = *b; поменяет местами адреса в локальных копиях указателей, а не значения.
  • Передача адреса несуществующей переменной. Адрес временного значения или уже освобождённой памяти приводит к неопределённому поведению.
  • Незаполненный выходной параметр. Если функция не записала в указатель, вызывающий прочитает мусор.

Best practices

  • Договоритесь о соглашении: входные параметры — по значению или const-указателю, выходные — по обычному указателю.
  • Проверяйте выходные указатели на NULL в начале функции, если они могут быть не заданы.
  • Документируйте, какие параметры функция изменяет. Это снижает число сюрпризов у тех, кто её вызывает.

Идею «функция меняет оригинал и возвращает несколько значений» в Python естественно выразить через возврат кортежа, но смоделируем именно C-подход с «выходными ячейками».

# Эмулируем выходные параметры через изменяемый словарь-память
def divmod_c(a, b, out):
    out["quot"] = a // b      # *quot = a / b
    out["rem"]  = a % b       # *rem  = a % b

result = {}
divmod_c(17, 5, result)       # передаём "адрес" out
print("Частное:", result["quot"], "Остаток:", result["rem"])

# swap через изменяемый список — аналог двух указателей
def swap(box):
    box[0], box[1] = box[1], box[0]

pair = [1, 2]
swap(pair)
print("После swap:", pair)    # [2, 1]

Та же логика на Python ▶ — изменяемый словарь/список играет роль памяти, доступной функции, как указатель в C. Так функция возвращает несколько результатов.

Указатели на функции

Указатель может хранить не только адрес данных, но и адрес функции. Это звучит экзотично, но лежит в основе многих приёмов C. Имея указатель на функцию, вы можете передавать поведение как параметр: например, стандартная сортировка qsort принимает указатель на функцию сравнения и потому умеет сортировать что угодно по любому критерию. Так в C реализуют обратные вызовы (callback): вы передаёте библиотеке свою функцию, и она вызывает её в нужный момент — на этом построены обработчики событий, плагины, таблицы команд. Синтаксис объявления поначалу пугает: int (*cmp)(int, int) — это «указатель на функцию, принимающую два int и возвращающую int». Но идея проста: функции тоже живут по адресам, а раз есть адрес — есть и указатель на него.

Итоги

Передавая указатель, функция получает доступ к оригиналу и может его менять. Это делает возможным корректный swap и возврат нескольких значений через выходные параметры. Ключевые операции — & при вызове (передать адрес) и * внутри функции (работать с оригиналом). Главные ошибки — забытые & или * и передача адресов несуществующих данных.

Проверьте себя
1. Почему swap через указатели меняет переменные местами, а наивный swap — нет?
AУказатели работают быстрее
BЧерез указатели функция получает адреса оригиналов и меняет сами x и y, а не их копии
CНаивный swap не компилируется
DУказатели автоматически удваивают значения
2. Как функция в C может «вернуть» несколько результатов сразу?
AЧерез несколько операторов return подряд
BНикак — функция всегда возвращает только одно
CЧерез выходные параметры-указатели, в которые она записывает результаты
DЧерез глобальные переменные — других способов нет