Передача аргументов: по значению
Урок раскрывает фундаментальный принцип 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 без них не работает. Никогда не возвращайте адрес локальной переменной.