Передача аргументов: по значению

Урок раскрывает фундаментальный принцип C: все аргументы передаются по значению, то есть копией. Это объясняет, почему функция «не видит» изменений переменных снаружи.
В C функция получает копию аргумента, а не сам аргумент. Изменения внутри функции касаются только копии. Чтобы изменить оригинал, нужно передать его адрес — указатель.

Рассмотрим функцию, которая пытается удвоить число:

void try_double(int x) {
    x = x * 2;    // меняем КОПИЮ
}

int main(void) {
    int n = 5;
    try_double(n);
    printf("%d\n", n);   // всё ещё 5, не 10!
    return 0;
}

Многих это удивляет: функция явно удвоила число, но снаружи оно не изменилось. Причина в том, что при вызове try_double(n) создаётся новая переменная x, в которую копируется значение n. Функция работает с этой копией. Оригинал n остаётся нетронутым.

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

Каждая переменная живёт по своему адресу в памяти. Передача по значению создаёт независимую копию по другому адресу:

main:                          try_double:
  n живёт по адресу A            x живёт по адресу B (другой!)
  [ n: 5 ]  --копируем->        [ x: 5 ]
                                    |
                                    | x = x * 2
                                    v
                                 [ x: 10 ]   меняется ТОЛЬКО копия

После возврата:
  [ n: 5 ]   оригинал не тронут
  [ x ]      копия уничтожена

Чтобы функция могла изменить оригинал, ей нужно передать адрес переменной — указатель. Тогда функция получит не копию значения, а «координаты» оригинала и сможет писать прямо в него:

void real_double(int *x) {   // принимаем АДРЕС
    *x = *x * 2;             // пишем по адресу — в оригинал
}

int main(void) {
    int n = 5;
    real_double(&n);         // передаём адрес n
    printf("%d\n", n);       // теперь 10!
    return 0;
}

Это полноценная тема следующего раздела, но важно понять связь уже сейчас: передача по значению — причина, по которой в C вообще нужны указатели для изменения переменных.

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

  • Ожидание, что функция изменит аргумент. Классическая попытка написать swap(a, b) без указателей не работает — меняются только копии.
  • Возврат адреса локальной переменной. return &local; — после выхода из функции эта память освобождается, адрес становится «висячим».
  • Путаница: массивы передаются «по-другому». Имя массива при передаче превращается в адрес — функция МОЖЕТ менять элементы. Это исключение из правила копирования (на самом деле копируется указатель).

Best practices

  • Если функция должна изменить переменную вызывающего — передавайте указатель и документируйте это в имени или комментарии.
  • Если функция только читает данные — передавайте по значению (для простых типов) или указатель на const (для больших структур).
  • Не возвращайте адреса локальных переменных. Возвращайте значение или используйте память, выделенную снаружи.

Python ведёт себя иначе (передаёт ссылки на объекты), поэтому смоделируем именно семантику C «копия значения» явно — через копирование примитива.

# Эмулируем передачу по значению (как простые типы в C)
def try_double(x):     # x — локальная копия
    x = x * 2
    return x           # чтобы увидеть результат, надо вернуть

n = 5
try_double(n)
print("После try_double, n =", n)   # всё ещё 5 — оригинал не тронут

# чтобы изменить — надо вернуть и присвоить (аналог return в C)
n = try_double(n)
print("После n = try_double(n):", n)  # теперь 10

Та же логика на Python ▶ — для чисел Python тоже фактически копирует значение. В C это правило универсально для всех простых типов: меняется только копия.

Почему массивы — исключение

Правило «всё передаётся по значению» имеет важное кажущееся исключение: массивы. Когда вы передаёте массив в функцию, изменения внутри функции видны снаружи — будто массив передан по ссылке. Но противоречия нет: на самом деле имя массива при передаче превращается в указатель на его первый элемент, и по значению копируется именно этот указатель. Функция получает копию адреса, но адрес-то указывает на тот же оригинальный массив, поэтому через него она и меняет настоящие элементы. Это объясняет сразу две вещи: почему функция не может узнать длину переданного массива (она получила лишь указатель, а не сам массив) и почему длину всегда передают отдельным параметром. Эта деталь — мостик к следующему разделу про указатели.

Итоги

В C все аргументы передаются по значению — функция получает копию. Изменения копии не влияют на оригинал. Чтобы изменить переменную вызывающего, нужно передать её адрес (указатель) и писать по нему через *. Это объясняет, зачем в C существуют указатели, и почему наивный swap без них не работает. Никогда не возвращайте адрес локальной переменной.

Проверьте себя
1. Почему функция try_double(int x) { x = x*2; } не изменяет переменную снаружи?
AПотому что умножение запрещено в функциях
BПотому что x — это копия аргумента; меняется копия, а оригинал остаётся нетронутым
CПотому что забыт return
DПотому что int нельзя умножать на 2
2. Как сделать так, чтобы функция реально изменила переменную вызывающего?
AОбъявить её глобальной — других способов нет
BПередать адрес переменной (указатель) и писать по нему через *
CДобавить слово static
DИспользовать тип double вместо int