Menu

Параметры функций в C: передача по значению, указатели и массивы

В C всё передаётся по значению — функции получают копии, а не оригиналы. Что это значит на практике, как указатели имитируют передачу по ссылке, почему массивы ведут себя иначе и как вернуть несколько результатов.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Параметры и аргументы

Два слова, которые используют как синонимы, но разделять их полезно:

  • Параметр — это переменная в определении функции: 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) возвращает признак успеха и заполняет оба выхода. Альтернатива — вернуть структуру, которая объединяет значения вместе.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ