Указатели и функции: изменяем аргументы
Урок показывает практическую силу указателей: как с их помощью функция меняет переменные вызывающего, как написать корректный 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 и возврат нескольких значений через выходные параметры. Ключевые операции — & при вызове (передать адрес) и * внутри функции (работать с оригиналом). Главные ошибки — забытые & или * и передача адресов несуществующих данных.