Параметры и аргументы
Два слова, которые используют как синонимы, но разделять их полезно:
- Параметр — это переменная в определении функции:
int nвint square(int n). - Аргумент — это значение, которое вы передаёте в месте вызова:
4вsquare(4).
Вызов функции создаёт её параметры как свежие локальные переменные и копирует в них аргументы. Эта копия и есть тема всей страницы.
C всегда передаёт по значению
Это правило, и исключений из него в C нет: функция получает копии своих аргументов. Присваивание параметру меняет только копию.
value по-прежнему равно 42. Функция получила копию по имени x, обнулила копию, и копия исчезла, когда функция завершилась. Переименование параметра в value ничего не изменит — имя их не связывает, это две разные переменные.
То же верно для любого типа: int, double, char, указателя и даже целой структуры (копируется поле за полем). Если вы пришли из Python, Java или JavaScript, учтите: там ссылки на объекты передаются по значению, поэтому изменение объекта внутри функции видно снаружи. В C такого нет — копируется сам объект.
Передача по значению — это преимущество, а не ограничение. Функция не может случайно испортить данные вызывающего кода, и рассуждать о программе становится гораздо проще.
Классический провал: swap
Хрестоматийная демонстрация — функция, которая пытается обменять два значения:
Логика верна — она действительно меняет местами a и b. Но a и b — копии, поэтому вся работа выбрасывается в тот же миг, когда функция завершается.
Имитация передачи по ссылке через указатели
Чтобы функция могла изменить переменную вызывающего кода, дайте ей адрес этой переменной, а не её значение. Сам адрес тоже передаётся по значению — указатель копируется, — но копия по-прежнему указывает на исходный объект, поэтому запись через неё достигает памяти вызывающего.
Всю идею несут три элемента синтаксиса:
int *aв списке параметров объявляетaкак «указатель наint».&xв месте вызова даёт адресx.*aвнутри функции означает «тотint, на который указываетa» — его можно читать и ему можно присваивать.
Именно это программисты на C называют «передачей по ссылке», и здесь стоит быть точным: это всё ещё передача по значению. Копируется указатель. Присваивание самому a (a = NULL;) изменило бы только копию; присваивание *a изменяет переменную вызывающего. Полную модель разбирают указатели.
Плата за это в том, что по месту вызова больше не видно, может ли переменная измениться, — и именно поэтому & обязателен. swap(x, y) не скомпилируется; вам придётся написать swap(&x, &y), и этот & служит видимой меткой того, что вызов может изменить x.
Возврат нескольких результатов
return выдаёт одно значение, поэтому дополнительные результаты передаются через параметры-указатели. Обычная форма — код состояния как возвращаемое значение и настоящие результаты, записанные через указатели:
Обратите внимание, что при неудачном вызове q и r сохраняют прежние значения — функция вернулась, ничего не записав. Это контракт, который вызывающий код обязан соблюдать, и именно поэтому проверка статуса идёт до использования результатов.
Альтернатива — вернуть структуру с обоими полями: она вовсе обходится без указателей и часто оказывается более удачным решением, когда значения действительно принадлежат друг другу.
Массивы ведут себя иначе
Массивы не подчиняются правилу копирования, и на этом спотыкаются почти все. Когда вы передаёте массив, он превращается в указатель на свой первый элемент, поэтому функция получает адрес, а не копию данных.
Массив вызывающего действительно изменился, причём без единого & — потому что data в этом контексте уже является адресом. Отсюда три следствия:
int a[] и int *a в списке параметров означают ровно одно и то же. Даже int a[100] тоже; размер игнорируется. Запись int a[] документирует намерение, но не читайте её как «массив копируется».
sizeof внутри функции даёт неверный результат. Вот этот баг:
В main sizeof(data) равен 20 байтам, и длина получается 5. Внутри функции sizeof(a) — это размер указателя, 8 на 64-битной системе, поэтому «длина» получается 2. Размера массива там просто нет. Всегда передавайте длину отдельным параметром. Современные компиляторы предупреждают об этом при -Wall.
Используйте const, когда писать не собираетесь. const int a[] говорит, что функция только читает, — компилятор это обеспечит, а читатель сможет на это положиться:
int sum(const int a[], int n); /* обещает не изменять массив */
Строки — тоже массивы, так что всё это применимо и к ним: void greet(const char *name) — стандартная сигнатура для функции, которая читает строку, не меняя её.
А что насчёт стоимости?
Передача большой структуры по значению копирует каждый её байт при каждом вызове. Для структуры из нескольких полей это нормально и часто даже быстрее, чем косвенность, которую добавляет указатель. Для большой — передавайте указатель и помечайте его const, если функция только читает:
struct Config { char name[64]; int flags[32]; double weights[128]; };
void applyConfig(const struct Config *cfg); /* без копирования 512 байт на вызов */
const struct Config *cfg означает «указатель на Config, который я не буду изменять», и это даёт эффективность указателя при безопасности копии.
Частые ошибки
- Ожидать, что обычный параметр изменит переменную вызывающего. Он не может. Передавайте указатель.
- Забыть
&в месте вызова.swap(x, y), когдаswapждёт указатели, — ошибка типов; компилятор её поймает, но только если в области видимости есть прототип. - Забыть
*внутри функции.a = b;для двух параметров-указателей меняет местами локальные копии, а не значения. Нужно*a = *b;. - Использовать
sizeofна параметре-массиве. Всегда неверно. Передавайте длину. - Возвращать указатель на локальную переменную. Локальная переменная умирает при возврате из функции, так что вызывающий получает висячий указатель, а это неопределённое поведение. Возвращайте по значению, пишите в буфер, предоставленный вызывающим, или выделяйте память через
malloc. - Не проверять параметр-указатель на
NULL. Функция, которая разыменовывает всё, что ей передали, упадёт на нулевом аргументе.
Часто задаваемые вопросы
Есть ли в C передача по ссылке?
Нет. В C ровно один режим: передача по значению. Функция всегда получает копию каждого аргумента, поэтому присваивание параметру никогда не затрагивает переменную вызывающего кода. То, что выглядит как передача по ссылке, — это передача указателя по значению: указатель копируется, но по-прежнему указывает на объект вызывающего, так что функция может изменить его через указатель.
Как передать массив в функцию в C?
Запишите параметр как int a[] или int *a — это одно и то же — и передавайте длину отдельным параметром: int sum(const int a[], int n). Массив не копируется; функция получает указатель на его первый элемент, поэтому изменения a[i] видны вызывающему коду.
Почему sizeof не работает с параметром-массивом в C?
Потому что параметр на самом деле является указателем. sizeof(a) внутри функции даёт размер указателя (обычно 8 байт), а не размер массива, поэтому sizeof(a)/sizeof(a[0]) выдаёт что-то вроде 2 вместо числа элементов. Всегда передавайте длину явно.
Как функция в C может вернуть больше одного значения?
Передавайте указатели для дополнительных результатов и пишите через них: int divide(int a, int b, int *quotient, int *remainder) возвращает признак успеха и заполняет оба выхода. Альтернатива — вернуть структуру, которая объединяет значения вместе.