Синтаксис определяет то, как должны правильно записываться языковые конструкции, в то время как семантика определяет значения языковых конструкций[1]. Синтаксис языка Си достаточно сложный, а семантика неоднозначная[2]. Основными двумя особенностями языка на момент его появления были унифицирование работы с массивами и указателями, а также схожесть того, как что-либо объявляется, с тем, как это в дальнейшем используется в выражениях[3]. Однако в последующем эти две особенности языка были в числе наиболее критикуемых[3], и обе являются сложными для понимания среди начинающих программистов[4]. Стандарт языка, определяя его семантику, не стал слишком сильно ограничивать реализации языка компиляторами, но этим самым сделал семантику недостаточно определённой. В частности, в стандарте есть 3 типа недостаточно определённой семантики: определяемое реализацией поведение, не заданное стандартом поведение и неопределённое поведение[5].
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z
Из допустимых символов формируются лексемы — предопределённые константы, идентификаторы и знаки операций. В свою очередь, лексемы являются частью выражений; а из выражений составляются инструкции и операторы.
При трансляции программы на Си из программного кода выделяются лексемы максимальной длины, содержащие допустимые символы. Если в программе имеется недопустимый символ, то лексический анализатор (или компилятор) выдаст ошибку, и трансляция программы окажется невозможной.
Символ # не может быть частью никакой лексемы и используется в препроцессоре➤.
Идентификаторы
Допустимый идентификатор — это слово, в состав которого могут входить символы латинского алфавита, цифры и знак подчёркивания[7]. Идентификаторы даются операторам, константам, переменным, типам и функциям.
В качестве идентификаторов программных объектов не могут использоваться идентификаторы ключевых слов и встроенные идентификаторы. Существуют и зарезервированные идентификаторы, на использование которых компилятор не выдаст ошибок, но которые в будущем могут стать ключевыми словами, что повлечёт за собой несовместимость.
Встроенный идентификатор только один — __func__, который определяется как константная строка, неявно объявляемая в каждой функции и содержащая её название[7].
Литеральные константы
Специально оформленные литералы в Си принято называть константами. Литеральные константы могут быть целочисленными, вещественными, символьными[8] и строковыми[9].
Целые числа по умолчанию задаются в десятичной системе счисления. Если указан префикс 0x, то — в шестнадцатеричной системе. Префикс в виде цифры 0 указывает, что число задаётся в восьмеричной системе. Суффикс определяет минимальный размер типа константы, а также определяет, является ли число знаковым или беззнаковым. В качестве итогового типа берётся такой минимально возможный, в котором данную константу можно представить[10].
Порядок назначения типов данных целым константам согласно их значению[10]
Суффикс
Для десятичных
Для восьмеричных и шестнадцатеричных
Нет
int
long
long long
int
unsigned int
long
unsigned long
long long
unsigned long long
u или U
unsigned int
unsigned long
unsigned long long
unsigned int
unsigned long
unsigned long long
l или L
long
long long
long
unsigned long
long long
unsigned long long
u или U вместе с l или L
unsigned long
unsigned long long
unsigned long
unsigned long long
ll или LL
long long
long long
unsigned long long
u или U вместе с ll или LL
unsigned long long
unsigned long long
Примеры записи вещественного числа 1.5
Десятичный
формат
С экспонентой
Шестнадцатеричный
формат
1.5
1.5e+0
0x1.8p+0
15e-1
0x3.0p-1
0.15e+1
0x0.cp+1
Константы вещественных чисел по умолчанию имеют тип double. При указании суффикса f константе назначается тип float, а при указании l или L — long double. Константа будет считаться вещественной, если в ней присутствует знак точки, либо буквы p или P в случае шестнадцатеричной записи с префиксом 0x. Десятичная запись может включать экспоненту, указываемую после букв e или E. В случае шестнадцатеричной записи экспонента указывается после букв p или P в обязательном порядке, что отличает вещественные шестнадцатеричные константы от целых. В шестнадцатеричном виде экспонента является степенью числа 2[11].
Символьные константы заключаются в одинарные кавычки ('), а префикс задаёт как тип данных символьной константы, так и кодировку, в которой символ будет представлен. В Си символьная константа без префикса имеет тип int[12], в отличие от C++, в котором символьной константе соответствует char.
Строковые литералы заключаются в двойные кавычки и могут иметь префикс, определяющий тип данных строки и её кодировку. Строковые литералы представляют собой обычные массивы. При этом в многобайтовых кодировках, таких как UTF-8, один символ может занимать более одного элемента массива. По факту строковые литералы являются константными[13], но в отличие от C++ их типы данных не содержат модификатор const.
Несколько подряд идущих строковых констант, разделённых пробельными символами или переводами строк объединяются в одну строку при компиляции, что часто используется для оформления кода строки путём разделения частей строковой константы по разным строкам для повышения читабельности[15].
В языке Си для задания констант принято использовать макроопределения, объявляемые с помощью директивы препроцессора➤#define[16]:
#defineимя константы [значение]
Введённая таким образом константа будет действовать в области своей видимости, начиная с момента задания константы и до конца программного кода или до тех пор, пока действие заданной константы не будет отменено директивой #undef:
#undefимя константы
Как и для всякого макроса, для именованной константы происходит автоматическая подстановка значения константы в программном коде всюду, где употреблено имя константы. Поэтому при объявлении внутри макроса целых или вещественных чисел может понадобиться явно указывать тип данных с помощью соответствующего суффикса литерала, иначе число по умолчанию будет иметь тип int в случае целого или тип double — в случае вещественного.
Для целых чисел существует другой способ создания именованных констант — через перечисления оператора enum[16]➤. Однако данный метод подходит только для типов, размером меньших либо равных типу int, и не используется в стандартной библиотеке[17].
Также можно создавать константы в виде переменных с квалификатором const, но в отличие от двух других способов, такие константы потребляют память, на них можно получить указатель, и их нельзя использовать на этапе компиляции[16]:
для указания размера битовых полей,
для задания размера массива (за исключением массивов переменной длины),
для задания значения элемента перечисления,
в качестве значения оператора case.
Ключевые слова
Ключевые слова — это идентификаторы, предназначенные для выполнения той или иной задачи на этапе компиляции, либо для подсказок и указаний компилятору.
Помимо ключевых слов стандарт языка определяет зарезервированные идентификаторы, использование которых может привести к несовместимости с будущими версиями стандарта. Зарезервированными являются все, за исключением ключевых, слова, начинающиеся со знака подчёркивания (_), после которого идёт либо заглавная буква (A—Z), либо другой знак подчёркивания[20]. В стандартах С99 и С11 часть таких идентификаторов была использована под новые ключевые слова языка.
В области видимости файла зарезервировано использование любых имён, начинающихся со знака подчёркивания (_)[20], то есть со знака подчёркивания допускается именовать типы, константы и переменные, объявленные в рамках какого-либо блока инструкций, например, внутри функций.
Также зарезервированными идентификаторами являются все макросы стандартной библиотеки и связываемые на этапе линковки названия из неё[20].
Использование зарезервированных идентификаторов в программах стандарт определяет как неопределённое поведение. Попытка отмены любого стандартного макроса через #undef также повлечёт за собой неопределённое поведение[20].
Комментарии
Текст программы на Си может содержать фрагменты, которые не являются частью программного кода, — комментарии. Комментарии специальным образом помечаются в тексте программы и пропускаются при компиляции.
Первоначально, в стандарте C89, были доступны встраиваемые комментарии, которые могли помещаться между последовательностями символов /* и */. При этом невозможно вложить один комментарий в другой, поскольку первая встреченная последовательность */ завершит комментарий, а текст, следующий непосредственно за обозначением */, будет воспринят компилятором как исходный текст программы.
Следующий стандарт, C99, ввёл ещё один способ оформления комментариев: комментарием считается текст, начинающийся с последовательности символов // и заканчивающийся концом строки[19].
Комментарии часто используются для самодокументирования исходного кода, поясняя работу сложных частей, описывая назначение тех или иных файлов, а также описывая правила использования и работу тех или иных функций, макросов, типов данных и переменных. Существуют постпроцессоры, которые умеют преобразовывать специально оформленные комментарии в документацию. Среди таких постпроцессоров с языком Си умеет работать система документирования Doxygen.
Операторы, применяемые в выражениях, представляют собой некоторую операцию, которая выполняется над операндами и которая возвращает вычисленное значение — результат выполнения операции. В качестве операнда может выступать константа, переменная, выражение или вызов функции. Оператор может представлять собой специальный символ, набор специальных символов или служебное слово. Операторы различают по количеству задействованных операндов, а именно — различают унарные операторы, бинарные операторы и тернарные операторы.
Унарные операторы
Унарные операторы выполняют операцию над единственным аргументом и имеют следующий формат операции:
[оператор] [операнд]
Операции постфиксного инкремента и декремента имеют обратный формат:
Получение количества байт, занимаемого объектом в памяти; может использоваться и как операция, и как оператор
-
Унарный минус
!
логическое отрицание
*
Разыменовывание указателя
--
Префиксный или постфиксный декремент
_Alignof
Получение выравнивания для заданного типа данных
Операторы инкремента и декремента, в отличие от остальных унарных операторов, изменяют значение своего операнда. Префиксный оператор сначала изменяет значение, а затем возвращает его. Постфиксный же сначала возвращает значение, а только потом его изменяет.
Бинарные операторы
Бинарные операторы располагаются между двумя аргументами и осуществляют операцию над ними:
Также к бинарным операторам в Си относятся лево-присваивающие операторы, которые производят операцию над левым и правым аргументом и заносят результат в левый аргумент.
Поразрядное исключающее ИЛИ правого операнда к левому
+=
Прибавление к левому операнду правого
/=
Деление левого операнда на правый
<<=
Поразрядный сдвиг левого операнда влево на количество бит, заданное правым операндом
-=
Вычитание из левого операнда правого
&=
Поразрядное И правого операнда к левому
>>=
Поразрядный сдвиг левого операнда вправо на количество бит, заданное правым операндом
*=
Умножение левого операнда на правый
|=
Порязрядное ИЛИ правого операнда к левому
Тернарные операторы
В Си имеется единственный тернарный оператор — сокращённый условный оператор, который имеет следующий вид:
[условие] ? [выражение1] : [выражение2]
Сокращённый условный оператор имеет три операнда:
[условие] — логическое условие, которое проверяется на истинность,
[выражение1] — выражение, значение которого возвращается в качестве результата выполнения операции, если условие истинно;
[выражение2] — выражение, значение которого возвращается в качестве результата выполнения операции, если условие ложно.
Оператором в данном случае является сочетание знаков ? и :.
Выражения
Выражение — это упорядоченный набор операций над константами, переменными и функциями. Выражения содержат операции, состоящие из операндов и операторов➤. Порядок выполнения операций зависит от формы записи и от приоритета выполнения операций. У каждого выражения имеется значение — результат выполнения всех операций, входящих в выражение. В ходе вычисления выражения в зависимости от операций могут изменяться значения переменных, а также могут исполняться функции, если их вызовы присутствуют в выражении.
Среди выражений выделяют класс лево-допустимых выражений — выражений, которые могут присутствовать слева от знака присваивания.
Приоритет выполнения операций
Приоритет операций определяется стандартом и задаёт порядок, в котором операции будут производиться. Операции в Си выполняются в соответствии приведённой ниже таблице приоритетов[24][25].
Приоритет
Лексемы
Операция
Класс
Ассоциативность
1
a[индекс]
Обращение по индексу
постфиксный
слева направо →
f(аргументы)
Вызов функции
.
Доступ к полю
->
Доступ к полю по указателю
++--
Положительное и отрицательное приращение
(имя типа){инициализатор}
Составной литерал (C99)
(имя типа){инициализатор,}
2
++--
Положительное и отрицательное префиксные приращения
Приоритеты операций в Си не всегда себя оправдывают и иногда приводят к интуитивно трудно предсказуемым результатам. Например, поскольку унарные операторы имеют ассоциативность справа налево, то вычисление выражения *p++ приведёт к увеличению указателя с последующим разыменовыванием (*(p++)), а не к увеличению значения по указателю ((*p)++). Поэтому в случае сложных для понимания ситуаций рекомендуется явно группировать выражения с помощью скобок[25].
Другой важной особенностью языка Си является то, что вычисление значений аргументов, передаваемых в вызов функции не является последовательным[26], то есть запятая, разделяющая аргументы, не соответствует последовательному вычислению из таблицы приоритетов. В следующем примере вызовы функций, указываемые в качестве аргументов другой функции, могут идти в произвольном порядке:
intx;x=compute(get_arg1(),get_arg2());// первым может быть вызов get_arg2()
Также нельзя полагаться на приоритет операций в случае наличия побочных эффектов, появляющихся в ходе вычисления выражения, поскольку это будет приводить к неопределённому поведению[26].
Точки следования и побочные эффекты
Приложение C стандарта языка определяет набор точек следования, в которых гарантируется отсутствие текущих побочных эффектов от вычислений. То есть точка следования — это этап вычислений, который разделяет вычисление выражений между собой так, что произошедшие до точки следования вычисления, включая побочные эффекты, уже закончились, а после точки следования — ещё не начинались[27].
Побочным эффектом может быть изменение значения переменной в ходе вычисления выражения. Изменение значения, участвующего в вычислениях, вместе с побочным изменением этого же значения до следующей точки следования будет приводить к неопределённому поведению. То же самое будет, если происходит два или более побочных изменений одного и того же значения, участвующего в вычислениях[26].
Операторы логического И (&&), ИЛИ (||) и последовательное вычисление (,)
Вычисление первого операнда
Вычисление второго операнда
Сокращённый оператор условия (?:)
Вычисление операнда, выступающего условием
Вычисление 2-го или 3-го операндов
Между двумя полными выражениями (не вложенными)
Одно полное выражение
Следующее полное выражение
Законченный полный описатель
Сразу перед возвратом из библиотечной функции
После каждого преобразования, связанного со спецификатором форматированного ввода-вывода
Сразу перед и сразу после каждого вызова функции сравнения, а также между вызовом функции сравнения и любыми перемещениями, выполняемыми над передаваемыми в функцию сравнения аргументами
инициализатор, не являющийся частью составного литерала;
обособленное выражение;
выражение, указанное в качестве условия условного оператора (if) или оператора выбора (switch);
выражение, указанное в качестве условия цикла while с предусловием или с постусловием;
каждый из параметров цикла for, если таковой указан;
выражение оператора return, если таковое указано.
В следующем примере переменная изменяется трижды между точками следования, что приводит к неопределённому результату:
inti=1;// Описатель - первая точка следования, полное выражение - втораяi+=++i+1;// Полное выражение - третья точка следованияprintf("%d\n",i);// Может быть выведено как 4, так и 5
Другие простые примеры неопределённого поведения, которого необходимо избегать:
i=i+++1;// неопределённое поведениеi=++i+1;// тоже неопределённое поведениеprintf("%d, %d\n",--i,++i);// неопределённое поведениеprintf("%d, %d\n",++i,++i);// тоже неопределённое поведениеprintf("%d, %d\n",i=0,i=1);// неопределённое поведениеprintf("%d, %d\n",i=0,i=0);// тоже неопределённое поведениеa[i]=i++;// неопределённое поведениеa[i++]=i;// тоже неопределённое поведение
Управляющие операторы
Управляющие операторы предназначены для осуществления действий и для управления ходом выполнения программы. Несколько идущих подряд операторов образуют последовательность операторов.
Пустой оператор
Самая простая языковая конструкция — это пустое выражение, называемое пустым оператором[28]:
;
Пустой оператор не совершает никаких действий и может находиться в любом месте программы. Обычно используется в циклах с отсутствующим телом[29].
Инструкции
Инструкция — это некое элементарное действие:
(выражение);
Действие этого оператора заключается в выполнении указанного в теле оператора выражения.
Несколько идущих подряд инструкций образуют последовательность инструкций.
Блок инструкций
Инструкции могут быть сгруппированы в специальные блоки следующего вида:
{
(последовательность инструкций)
},
Блок инструкций, также иногда называемый составным оператором, ограничивается левой фигурной скобкой ({) в начале и правой фигурной скобкой (}) — в конце.
В функциях➤ блок инструкций обозначает тело функции и является частью определения функции. Также составной оператор может использоваться в операторах циклов, условия и выбора.
Условные операторы
В языке существует два условных оператора, реализующих ветвление программы:
оператор if, содержащий проверку одного условия,
и оператор switch, содержащий проверку нескольких условий.
Самая простая форма оператора if
if((условие)) (оператор)
(следующий оператор)
Оператор if работает следующим образом:
если выполнено условие, указанное в скобках, то выполняется первый оператор, и затем выполняется оператор, указанный после оператора if.
если условие, указанное в скобках, не выполнено, то сразу выполняется оператор, указанный после оператора if.
В частности, следующий ниже код, в случае выполнения заданного условия, не будет выполнять никаких действий, поскольку, фактически, выполняется пустой оператор:
if((условие));
Более сложная форма оператора if содержит ключевое слово else:
if((условие)) (оператор)
else (альтернативный оператор)
(следующий оператор)
Здесь, если условие, указанное в скобках, не выполнено, то выполняется оператор, указанный после ключевого слова else.
Несмотря на то, что стандарт допускает указание тела операторов if или else одной строкой, это считается плохим стилем, снижающим читабельность кода. В качестве тела рекомендуется всегда указывать блок инструкций с помощью фигурный скобок[30].
Операторы выполнения цикла
Цикл — это фрагмент программного кода, содержащий
условие выполнения цикла — условие, которое постоянно проверяется;
и тело цикла — простой или составной оператор, выполнение которого зависит от условия цикла.
В соответствии с этим, различают два вида циклов:
цикл с предусловием, где сначала проверяется условие выполнения цикла, и, если условие выполнено, то выполняется тело цикла;
цикл с постусловием, где проверка условия продолжения цикла происходит после исполнения тела цикла.
Цикл с постусловием гарантирует, что тело цикла выполнится по крайней мере один раз.
В языке Си предусмотрено два варианта циклов с предусловием: while и for.
Цикл for ещё называется параметрическим, он эквивалентен следующему блоку операторов:
[блок инициализации]
while(условие)
{
[тело цикла]
[оператор]
}
В обычной ситуации блок инициализации содержит задание начального значения переменной, которая называется переменной цикла, а оператор, который выполняется сразу после тела цикла, меняет значения используемой переменной, условие содержит сравнение значения используемой переменной цикла с некоторым заранее заданным значением, и, как только сравнение перестаёт выполняться, цикл прерывается, и начинает выполняться программный код, следующий сразу за оператором цикла.
У цикла do-while условие указывается после тела цикла:
do [тело цикла] while(условие)
Условие цикла — это логическое выражение. Однако неявное приведение типов позволяет использовать в качестве условия цикла арифметическое выражение. Это позволяет организовать так называемый «бесконечный цикл»:
while(1);
То же самое можно сделать и с применением оператора for:
for(;;);
На практике такие бесконечные циклы обычно используются совместно с операторами break, goto или return, которые осуществляют прерывание работы цикла разными способами.
Как и для оператора условия, использование однострочного тела без заключения его в блок инструкций с помощью фигурных скобок считается плохим стилем, снижающим читабельность кода[30].
Операторы безусловного перехода
Операторы безусловного перехода позволяют прервать выполнение любого блока вычислений и перейти в другое место программы в рамках текущей функции. Операторы безусловного перехода обычно используются совместно с условными операторами.
goto [метка],
Метка — это некоторый идентификатор, передаёт управление тому оператору, который помечен в программе указанной меткой:
[метка] : [оператор]
Если указанная метка отсутствует в программе или если существует несколько операторов с одной и той же меткой, компилятор сообщает об ошибке.
Передача управления возможна только в пределах той функции, где используется оператор перехода, следовательно, при помощи оператора goto нельзя передать управление в другую функцию.
Другие операторы перехода связаны с циклами и позволяют прервать выполнения тела цикла:
оператор break немедленно прерывает выполнение тела цикла, и происходит передача управления на оператор, следующий непосредственно сразу за циклом;
оператор continue прерывает выполнение текущей итерации цикла и инициирует попытку перехода к следующей.
Оператор break также может прерывать работу оператора switch, поэтому внутри оператора switch, запущенного в цикле, оператор break не сможет прервать работу цикла. Указанный в теле цикла, он прерывает работу ближайшего вложенного цикла.
Оператор continue может быть использован только внутри операторов do, while и for. У циклов while и do-while оператор continue вызывает проверку условия цикла, а в случае цикла for — исполнение оператора, заданного в 3-м параметре цикла, перед проверкой условия продолжения цикла.
Оператор возврата из функции
Оператор return прерывает выполнение той функции, в которой использован. Если функция не должна возвращать значение, то используется вызов без возвращаемого значения:
return;
Если функция должна возвращать какое-либо значение, то после оператора указывается возвращаемое значения:
return[значение];
Если после оператора возврата в теле функции имеются ещё какие-то операторы, то эти операторы никогда не будут выполняться, и в этом случае компилятор может выдать предупреждение. Однако после оператора return могут указываться инструкции для альтернативного завершения функции, например, по ошибке, а переход к этим операторам можно осуществлять с помощью оператора goto согласно каким-либо условиям➤.
Переменные
При объявлении переменной указывается её тип➤ и название, а также может указываться начальное значение:
[описатель] [имя] ;
или
[описатель] [имя] = [инициализатор] ;,
где
[описатель] — тип переменной и предшествующие типу необязательные модификаторы;
[имя] — имя переменной;
[инициализатор] — начальное значение переменной, присваиваемое при её создании.
Если переменной не присвоено начальное значение, то в случае глобальной переменной её значение заполняется нулями, а для локальной переменной начальное значение будет неопределённым.
В описателе переменной можно обозначать переменную как глобальную, но ограниченную областью видимости файла или функции, с помощью ключевого слова static. Если переменная объявлена глобальной без ключевого слова static, то обращаться к ней возможно и из других файлов, где требуется объявить данную переменную без инициализатора, но с ключевым словом extern. Адреса таких переменных определяются на этапе компоновки.
Функции
Функция — это самостоятельный фрагмент программного кода, который может многократно использоваться в программе. Функции могут иметь аргументы и могут возвращать значения. Также функции могут иметь побочные эффекты при своём исполнении: изменение глобальных переменных, работа с файлами, взаимодействие с операционной системой или оборудованием[27].
Для того, чтобы задать функцию в Си, необходимо её объявить:
сообщить имя (идентификатор) функции,
перечислить входные параметры (аргументы)
и указать тип возвращаемого значения.
Также необходимо привести определение функции, которое содержит блок операторов, реализующих поведение функции.
Отсутствие объявления определённой функции является ошибкой, если функция используется вне области видимости определения, что, в зависимости от реализации, приводит к выдаче сообщений или предупреждений.
Для вызова функции достаточно указать её имя с параметрами, указанными в скобках. При этом адрес точки вызова помещается в стек, создаются и инициализируются переменные, отвечающие за параметры функции, и передаётся управление коду, реализующему вызываемую функцию. После выполнения функции происходит освобождение памяти, выделенной при вызове функции, возврат в точку вызова и, если вызов функции является частью некоторого выражения, передача в точку возврата вычисленного внутри функции значения.
Если после функции не указаны скобки, то компилятор интерпретирует это как получение адреса функции. Адрес функции можно заносить в указатель и в последующем вызывать функцию посредством указателя на неё, что активно используется, например, в системах плагинов[31].
С помощью ключевого слова inline можно помечать функции, вызовы которых требуется исполнять как можно быстрее. Компилятор может подставлять код таких функций непосредственно в точку их вызова[32]. С одной стороны, это увеличивает объём исполняемого кода, но, с другой, — позволяет экономить время его выполнения, поскольку не используется дорогостоящая по времени операция вызова функции. Однако из-за особенностей построения архитектуры компьютеров, встраивание функций может приводить как к ускорению, так и к замедлению работы приложения в целом. Тем не менее во многих случаях встраиваемые функции являются предпочтительной заменой макросам[33].
Объявление функции
Объявление функции имеет следующий формат:
[описатель] [имя] ( [список] );,
где
[описатель] — описатель типа возвращаемого функцией значения;
[имя] — имя функции (уникальный идентификатор функции);
[список] — список (формальных) параметров функции или void при их отсутствии[34].
Признаком объявления функции является символ «;», таким образом, объявление функции — это инструкция.
В самом простом случае [описатель] содержит указание на конкретный тип возвращаемого значения. Функция, которая не должна возвращать никакого значения, объявляется как имеющая тип void.
При необходимости в описателе могут присутствовать модификаторы, задаваемые с помощью ключевых слов:
extern указывает на то, что определение функции находится в другом модуле➤;
static задаёт статическую функцию, которая может быть использована только в текущем модуле.
Список параметров функции задаёт сигнатуру функции.
Си не допускает объявление нескольких функций, имеющих одно и то же имя, перегрузка функций не поддерживается[35].
Определение функции
Определение функции имеет следующий формат:
[описатель] [имя] ( [список] ) [тело]
Где [описатель], [имя] и [список] — те же, что и в объявлении, а [тело] — это составной оператор, который представляет собою конкретную реализацию функции. Компилятор различает определения одноимённых функций по их сигнатуре, и таким образом (по сигнатуре) устанавливается связь между определением и соответствующим ему объявлением.
Тело функции имеет следующий вид:
{
[последовательность операторов]
return ([возвращаемое значение]) ;
}
Возврат из функции осуществляется с помощью оператора return➤, у которого либо указывается возвращаемое значение, либо не указывается, в зависимости от возвращаемого функцией типа данных. В редких случаях функция может быть помечена как не делающая возврат с помощью макроса noreturn из заголовочного файла stdnoreturn.h, в таких случаях оператор return не требуется. Например, подобным образом можно помечать функции, безусловно вызывающие внутри себя abort()[32].
Вызов функции
Вызов функции заключается в выполнении следующих действий:
сохранение точки вызова в стеке;
автоматическое выделение памяти➤ под переменные, соответствующие формальным параметрам функции;
инициализация переменных значениями переменных (фактических параметров функции), переданных в функцию при её вызове, а также инициализация тех переменных, для которых в объявлении функции указаны значения по умолчанию, но для которых при вызове не были указаны соответствующие им фактические параметры;
передача управления в тело функции.
В зависимости от реализации, компилятор либо строго следит за тем, чтобы тип фактического параметра совпадал с типом формального параметра, либо, если существует такая возможность, осуществляет неявное преобразование типа, что, очевидно, приводит к побочным эффектам.
Если в функцию передаётся переменная, то при вызове функции создаётся её копия (в стеке выделяется память и копируется значение). Например, передача структуры в функцию вызовет копирование всей структуры целиком. Если же передаётся указатель на структуру, то копируется только значение указателя. Передача в функцию массива также вызывает лишь копирование указателя на его первый элемент. При этом для явного обозначения того, что на вход функции принимается адрес начала массива, а не указатель на единичную переменную, вместо объявления указателя после названия переменной можно поставить квадратные скобки, например:
voidexample_func(intarray[]);// array — указатель на первый элемент массива типа int
Си допускает вложенные вызовы. Глубина вложенности вызовов имеет очевидное ограничение, связанное с размером выделяемого программе стека. Поэтому в реализациях Си устанавливается некое предельное значение для глубины вложенности.
Частный случай вложенного вызова — это вызов функции внутри тела вызываемой функции. Такой вызов называется рекурсивным, и применяется для организации единообразных вычислений. Учитывая естественное ограничение на вложенные вызовы, рекурсивную реализацию заменяют на реализацию при помощи циклов.
Примечания
Комментарии
↑Макрос bool из заголовочного файла stdbool.h является обёрткой над ключевым словом _Bool.
↑Макрос complex из заголовочного файла complex.h является обёрткой над ключевым словом _Complex.
↑Макрос imaginary из заголовочного файла complex.h является обёрткой над ключевым словом _Imaginary.
↑Макрос alignas из заголовочного файла stdalign.h является обёрткой над ключевым словом _Alignas.
↑ 12Макрос alignof из заголовочного файла stdalign.h является обёрткой над ключевым словом _Alignof.
↑Макрос noreturn из заголовочного файла stdnoreturn.h является обёрткой над ключевым словом _Noreturn.
↑Макрос static_assert из заголовочного файла assert.h является обёрткой над ключевым словом _Static_assert.
↑Макрос thread_local из заголовочного файла threads.h является обёрткой над ключевым словом _Thread_local.
ISO/IEC.ISO/IEC9899:2017. Programming languages — C (неопр.). www.open-std.org (2017). Дата обращения: 3 декабря 2018. Архивировано из оригинала 24 октября 2018 года.