Выбираем стандарт языка Си в 2025 году

Шёл 2025 год, а я задался вопросом: «Не пора ли нашей команде выбрать стандарт языка Си, на котором будет вестись основная часть разработки?» С одной стороны кажется, что этот вопрос давно должен быть решён, стандарт языка указан в code style, конечно, после «холиваров», череды обсуждений, обид, проклятий и прочих маленьких трагедий. А если нет? А если нет, то так ли это важно? Что там нового может быть в языке Си? Стоит ли этот вопрос вообще того, чтобы тратить время? В этой заметке поделюсь с уважаемым читателем тем интересным, что я узнал и вспомнил во время неспешного исследования этого вопроса, относительно холодными и тёмными вечерами.

Заметка не претендует на окончательный и безоговорочный ответ на вопрос выбора стандарта языка. Вместо этого я просто опишу свои мысли на тему. Конечно выбор стандарта зависит от ряда условий, например, что именно разрабатывается, с какой целью, кто разрабатывает и т.п. Понятно, что для командной разработки кросс-платформенной библиотеки, которая должна работать везде, здесь и сейчас, а также немного в прошлом, и для разработки собственного пет-проекта, который предполагается собирать в заранее известном окружении, на выбор стандарта языка будут в разной мере оказывать влияние разные факторы. Поэтому надеюсь, что читатель не будет ждать чёткого и однозначного ответа, какой же стандарт всё-таки выбрать в качестве руководства к действию. Возможно не столько мой ответ, сколько мои размышления, которые к этому ответу привели, будут интересны чуть более широкой аудитории. Также надеюсь, что читатели простят мне некоторые не самы удачные шутки, которые я позволил себе добавить в текст заметки. Тем не менее, на всякий случай, готовьте помидоры и тухлые яйца, а я пока надену полиэтиленовый дождевик.

Чтобы было немного понятно, с кем имеет дело уважаемый читатель, позвольте сообщить несколько фактов о себе. Занимаюсь программированием почти 30 лет, с середины 90-х годов прошлого века начал учить язык Си (и продолжаю это делать все эти годы хоть и не всегда регулярно), с конца 90-х того же века переехал с Windows на Linux (вообще люблю и уважаю UNIX-подобные системы), с начала 00-х текущего тысячелетия занимаюсь системной разработкой и операционными системами. Пожалуй, хватит обо мне, давайте лучше отправимся в небольшое и местами немного археологическое путешествие в прошлое и попробуем вспомнить откуда есть пошёл язык Си. «Что опять?» — скажет читатель. «Да сколько же можно мусолить эту тему?» Тем не менее прошу потерпеть, кажется, что без этого нам сегодня не обойтись.

Первобытное общество. Первый стандарт де-факто — K&R C

В 1969 году AT&T Bell Labs начинает разработку UNIX и Си. Через два года в 1971 году компилятор языка Си уже включён в UNIX редакции 2. Ещё через два года в 1973 году ядро UNIX переписано на Си. Собственно это и была одна из задач нового языка, который разработали братья Деннис Макалистэйр Ритчи и Брайан Уилсон Керниган — упрощение переноса ОС UNIX на другие платформы. Искушённый читатель, конечно, скажет, что это неправда. Соглашусь, братьями Керниган и Ритчи, конечно, не были. Тем не менее язык получился довольно простым в понимании (ага, щаз), компактным и его компиляторы стали появляться для разных платформ. В 1978 году вышла книга от авторов языка — The C Programming Language. Это по сути и есть первый стандарт де-факто языка Си, который иногда называют K&R C.

Рисунок 1. Брайан Керниган и Деннис Ритчи смотрят на своё творение.
Рисунок 1. Брайан Керниган и Деннис Ритчи смотрят на своё творение.

На рис. 1 слева Керниган, справа Ритчи, и смотрите не перепутайте. На самом деле эти фотографии найдены в сети, и скорее всего авторы языка в то время могли выглядеть немного иначе. Книга, кстати, достаточно редкая, в отличие от второго издания (хотя не будем забегать вперёд).

Древний мир. ANSI C, или C89, или C90. И ещё C95

Язык Си продолжает набирать популярность. Причины, наверное, понятны — на Си писать проще, чем на ассемблере, и очень часто надо написать только один раз, а потом программу достаточно только скомпилировать на любой платформе, для которой есть компилятор языка Си. Язык продолжает развиваться и потихоньку обрастать новыми возможностями, появляются компиляторы, которы часто не полностью совместимы друг с другом. Так программа написанная с использованием расширений одного компилятора уже не могла без модификации собираться другим компилятором. А это проблема, ведь по сути именно этого хотели избежать Керниган и Ритчи, когда разрабатывали язык Си как замену ассемблера для системной разработки. В это время, а именно в 1983 году, выходит из спячки Американский национальный институт стандартов (в народе известный как ANSI) и решает, что пора возглавить всё это безобразие. И создаёт, нет, не стандарт языка Си, а комитет для разработки спецификации Си. Шесть лет комитет работал над стандартом ANSI X3.159–1989, который увидел свет в 1989 году. Этот стандарт известен как ANSI C или C89.

Наблюдательный читатель, наверное, заметил, что на разработку языка Си, разработку компилятора для языка и переписывания ядра UNIX на Си потребовалось 4 года, а на разработку стандарта — 6 лет. Хотя, конечно, во втором случае в работе принимали участие и институт, и комитет.

Керниган и Ритчи всё ещё очень активны в то время и, не откладывая дело в долгий ящик, ещё в 1989 году выпускают вторую редакцию свой замечательной книги «The C Programming Language». Чтобы народ не сомневался, что книгу надо покупать, на обложке красуется штампик — ANSI C.

Рисунок 2. Брайан Керниган и Деннис Ритчи смотрят на читателей своей книги
Рисунок 2. Брайан Керниган и Деннис Ритчи смотрят на читателей своей книги

На рис. 2 те же действующие лица, что и на рис. 1. Ритчи (справа) тогда потерял свои очки, поэтому немного сердится, а Керниган (слева) очки не только не терял, но ещё и нашёл, поэтому так улыбается. На самом деле это фотографии из сети более позднего времени, только для иллюстрации заметки. Именно эта книга претерпела множество переизданий, в т.ч. и на русском языке. Мамонты из 90-х учили Си скорее всего по этой книге. Хотя я почему-то запомнил книгу братьев Березиных.

Рисунок 3. Неплохая книга по языку Си из 90-х.
Рисунок 3. Неплохая книга по языку Си из 90-х.

На рис. 3 можно увидеть обложку книги «Начальный курс C и C++», которую написали братья Березины — Борис Иванович и Сергей Борисович. Кстати, это не такое редкое явление, когда книгу пишут люди с одинаковой фамилией, можно вспомнить, например, «Компьютерные сети» от Олифер и Олифер. Но не будем отвлекаться.

В 1990 году вышла из спячки уже Международная организация по стандартизации (в народе известная как ISO) и решила, что не дело это такому красивому стандарту C89 быть только американским, поэтому организовала рабочую группу №14 (WG14) для работы над стандартом языка Си. С небольшими отличиями от C89 ISO принимает стандарт ISO/IEC 9899:1990 известный как C90. Позже были выпущены два исправления для C90. В 1994 году — ISO/IEC 9899:1990/Cor 1:1994 TCOR1, а в 1996 году — ISO/IEC 9899:1990/Cor 2:1996. И уже совсем неожиданным становится выпуск в 1995 году ISO/IEC 9899:1990/AMD1:1995 или Amendment 1 или C95. Тут можно обратить внимание на два фактора. Во-первых, стандарты выходят в том году, номер которого присутствует в номере стандарта, что в общем-то хорошо, но в дальнейшем не всегда будет соблюдаться. Во-вторых, уже начинается чехарда с номерами — C90 выходит позже C95. Почему бы не назвать C95 Cor 2, а Cor 2 — Cor 3?

Надо всё-таки отметить, что стандарты ANSI C89 и ISO C90 несколько отличаются. Но за давностью лет эти отличия уже не так важны, и в наше время даже компиляторы иногда не помнят как правильно и предлагают включать режим C89/C90.

Отличия ANSI C от K&R C

На самом деле выявить отличия между K&R C и ANSI C может быть не так просто. Во-первых, прошло более 35 лет (а по некоторым подсчётам более 45). Во-вторых, первое издание книги Кернигана и Ритчи сейчас не так просто добыть, т.к. обычно предлагается второе (ANSI C). Но кое-какую информацию получить удалось, ниже приведены некоторые изменения в ANSI C:

  • добавлен квалификатор типа volatile

  • добавлен квалификатор типа const

  • добавлен спецификатор типа void

  • добавлен спецификатор типа signed

  • добавлен новый тип long double

  • упразднён long float

  • добавлен суффикс U

  • упразднены восьмеричные 8 и 9

  • добавлено многоточие … для вариативных функций

  • инкремент/декремент += и -= вместо =+ и =-

«Как же, позвольте?… Он служил в очистке…» © Ладно volatile, а особенно signed, но как же без const и без void? Разве может читатель представить себе добрую уютную сишечку без void? Разумное объяснение этому всё-таки есть. Действительно в первом издании книги Кернигана и Ритчи всё было проще. Но язык развивался постепенно с 1978 года по 1989, и перечисленные выше возможности были добавлены не все сразу в 1989 году, а всё это время появлялись в разных диалектах языка в том или ином виде. В ANSI C эти возможности сделали обязательными (или наоборот упразднили) для всех компиляторов, которые соответствуют стандарту. Кстати, зачастую новые возможности в этом и последующих стандартах Си это как раз узаконивание расширений, которые были реализованы неофициально.

Средние века. С99

ISO раскочегарилась по полной и, размявшись на C90, его корректировках и C95, в 1999 году выпускает стандарт ISO/IEC 9899:1999 известный как C99. К тому времени ANSI ещё пытается сохранить лидерство и выпускает аналогичный стандарт в 2000 году.

Для C99 были выпущены три корректировки. В 2001 году — ISO/IEC 9899:1999/Cor 1:2001(E), в 2004 году — ISO/IEC 9899:1999/Cor 2:2004(E), а в 2007 году — ISO/IEC 9899:1999/Cor 3:2007(E), которая примечательна тем, что в ней функция gets () объявляется устаревшей (и вовсе удаляется уже в следующем стандарте C11).

С 2011 года стандарт C99 больше не поддерживается ни ANSI, ни ISO, т.к. выпущен новый стандарт C11.

Отличия C99 от ANSI C

Отличия C99 от своего предшественника более понятные, хотя некоторым могут показаться неожиданными, часть изменений приведена ниже:

  • новые типы — long long, _Bool, _Complex

  • встраиваемые функции inline

  • объявление переменных не ограничено началом блока

  • массивы переменной длины (VLA)

  • поддержка однострочных комментариев //

  • составные константы:
    calculate ((struct point){ 4, 2 })

  • инициализаторы массивов:
    int а[10] = { [0] = 100, [3] = 200 };

  • инициализаторы структур:
    struct data { int a; int b; };
    data = { .c = 30, .a = 10 };

  • вариативные макросы:
    #define eprintf (…) fprintf (stderr, __VA_ARGS__)

Занятно, что однострочные комментарии // не были включены в K&R C, но существовали в языке BCPL (Before C Programming Language), который был одним из предков языка Си, хотя некоторые считают, что однострочные комментарии это ноу-хау C++. Тем не менее в C99 историческая справедливость была восстановлена.

Дотошный читатель наверняка обратит внимание, что новые ключевые слова начинаются с символа подчёркивания и заглавной буквы. Немного позднее этот страх будет улучшено в C23.

Новое время. C11 и C17

В том же самом 2007 году, в котором выходит Cor 3 для C99, начинается работа над новым стандартом, который получил условное обозначение C1x. Это значит, что публикация стандарта ожидается в 2010-х годах. И ожидания не подвели, в 2011 году выходит стандарт ISO/IEC 9899:2011 или C11. Для этого стандарта была выпущена одна корректировка — ISO/IEC 9899:2011/Cor 1:2012.

Следующий стандарт языка Си выпущен в 2018 году — ISO/IEC 9899:2018. Тем не менее стандарт называется C17, хотя иногда ошибочно его называют C18. «Но для чего рассказывать о двух стандартах в одном разделе?» — спросит читатель. А потому, что C17 принципиально новых возможностей не добавляет и содержит в основном исправление формулировок и неточностей C11. Иногда оба стандарта объединяют под именем C11/C17. Среди прочего в C17 однозначно разрешается вызывать функцию realloc () с нулевым размером памяти, но в C23 это будет изменено.

Отличия C11 и C17 от C99

Некоторые изменения языка Си в стандарте C11 приведены ниже, а с более подробным описанием изменений можно ознакомиться по ссылке:

  • удалена функция gets ()

  • выравнивание данных: _Alignas, _Alignof, aligned_alloc ()

  • спецификатор _Noreturn

  • выражения, независящие от типа: _Generic

  • спецификатор типа _Thread_local и 

  • атомарные операции доступа памяти _Atomic и 

  • анонимные структуры и объединения:

struct T {
    int tag;
    union {
        float x;
        int n;
    };
};
  • статические утверждения

  • привилегированный режим x для fopen ()

  • функция quick_exit ()

  • выборочные возможности

Обратили внимание, что в C11 появились макросы выборочных возможностей? Теперь компилятор может официально (т.е. в соответствии со стандартом) поддерживать стандарт не в полном объёме. И что интересно, некоторые возможности стандарта C99 стали опциональными в C11.

Новейшее время. C23

Уже в 2016 году появилось неофициальное название следующего стандарта — C2x. Ага, значит стандарт будет опубликован в 2020-х. В 2019 году состоялась первая встреча рабочей группы по языку Си посвящённая будущему стандарту. И спустя пять лет и одну эпидемию COVID-19 стандарт ISO/IEC 9899:2024 или C23 был наконец-то утверждён. Групповая фотография (изображение увеличено этими вашими нейросетями) отважных людей, которые утвердили стандарт показана на рис. 4. Кстати, следующий стандарт языка Си ожидается относительно скоро, т.к. имеет неофициальное название C2y.

Рисунок 4. Члены WG14, лично принявшие участие в Страсбургской встрече по финализации C23.
Рисунок 4. Члены WG14, лично принявшие участие в Страсбургской встрече по финализации C23.

Кажется, что на Хабре должна была быть статься по новому стандарту C23, но я почему-то не нашёл.

Отличия C23 от C11 и C17

Поскольку стандарт C23 был опубликован относительно недавно, а именно в октябре 2024 года, то перечень изменений C23 выглядит относительно большим. Но это, наверное, с непривычки, т.к. к предыдущему стандарту, разработчики за десяток с лишним лет уже привыкли. Кратко рассмотрим изменения.

Посмотрим, что удалено из стандарта:

  • обязательная поддержка чисел с дополнительным кодом

  • определение функций в стиле K&R

  • вызов realloc () с 0 теперь неопределённое поведение (ха-ха)

Нововведения языка:

  • директивы препроцессора #embed, #elifdef, #elifndef, #warning (пора)

  • типы _Decimal32, _Decimal64 и _Decimal128

  • атрибуты в стиле C++11:
    [[nodiscard]], [[maybe_unused]], [[deprecated]], [[fallthrough]], [[noreturn]], [[reproducible]], [[unsequenced]]

  • метки могут появляться до объявлений и в конце выражений

  • неименованные параметры в объявлении функций:
    int f (int, int) { return 7; }

  • бинарные литералы: 0b10101010

  • разделители цифр: 0xFF«FF"FF«FF

  • оператор typeof ()

  • пустая инициализация с помощью {} (включая VLA)

  • ключевое слово auto для объявления переменных

  • спецификатор constexpr

  • ключевое слово static_assert вместо _Static_assert

  • ключевое слово thread_local вместо _Thread_local

  • ключевые слова alignas, alignof, bool, true, false

Изменения в стандартной библиотеке:

  • новые заголовочные файлы и 

  • некоторые POSIX функции становятся стандартными:
    memccpy (), strdup () (наконец-то) и strndup ()
    gmtime_r (), localtime_r ()

  • расширения fscanf () и fprintf ():
    спецификатор %b для вывода бинарных чисел
    H, D, DD для _Decimal32, _Decimal64 и _Decimal128

Можно заметить, что ряд ключевых слов изменились с формата символ подчёркивания и заглавная буква на строчное написание. И в целом стандарт привносит много интересных и полезных возможностей, а также сближает язык Си со стандартом C++11.

Макросы версии

Сложно представить, что уважаемый читатель не знает, как при помощи макроса __STDC_VERSION__ определить текущую версию стандарта языка Си на этапе компиляции программы. Но всё-таки оставлю здесь этот фрагмент кода в качестве шпаргалки:

#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
/* C23 compatible source code. */
#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201710L
/* C17 compatible source code. */
#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
/* C11 compatible source code. */
#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L
/* C99 compatible source code. */
#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199409L
/* C95 compatible source code. */
#elif defined(__STDC__) && __STDC__
/* C89 compatible source code. */
#else
/* K&R C compatible source code. */
#endif  

Поддержка стандартов языка Си в компиляторах

Кажется, что пора делать выбор стандарта языка Си, который будет использоваться для разработки в команде. Но не стоит спешить, есть ещё ряд факторов, которые следует учесть. Для начала это поддержка стандарта языка компилятором.

Поскольку я мало сталкиваюсь с разработкой под Windows, то с компилятором фирмы Microsoft знаком не очень глубоко. Однако, наслышан, что новые стандарты языка компилятор не спешит поддерживать. Как я понимаю, поддержка C11 и C17 появилась в MS Visual Studio 2019 (и то не сразу), а  стал доступен в MS Visual Studio 2022. С подробностями можно ознакомиться в, например, документации здесь и здесь.

Компилятор GNU GCC поддерживает различные стандарты языка Си в разном объёме. Чем старше стандарт, тем более полно он поддерживается. Что было в прошлом тысячелетии это тайна покрытая мраком. Нет, конечно, можно взять лопату и раскопать это дело, было бы интересно, но с практической точки зрения не особо полезно. Ниже приведу информацию по стандарту языка Си, который используется компилятором по умолчанию. Некоторая информация по этому вопросу доступна в документации здесь и здесь.

Компилятор Clang тоже поддерживает стандарты языка Си в разной степени полноты. Стоит отметить что поддержка стандарта ANSI C заявлена в полном объёме чуть ли не с первой версии компилятора. Стандарт C99 поддерживается начиная с версии 3.0, а уже в 17 версии заявлена полная поддержка стандарта. Последующие стандарты языка Си поддерживаются частично, при этом C11 и C17 (компилятор не делает различий между стандартами) поддерживаются начиная с версии 3.0, а стандарт C23 — с версии компилятора 18. Полноту поддержки разных стандартов языка Си можно оценить по информации на этой странице.

Следующая таблица даёт примерное представление о том, какая версия компилятора на какую редакцию стандарта языка Си ориентирована.

Стандарт

GCC

Clang

C89 и C90

2.95.3 (2001) — 4.9.4 (2016)

+

C99

-

3.0 — 17

C11

5.0 (2017) — 7.5 (2019)

3.0 — 17

C17

8.0 (2021) — 14.2 (2024)

аналогично C11

C23

-

18 — …

Помимо компиляторов стоит учесть поддержку стандарта языка и в других инструментах, который используются при разработке — в статических анализаторах, форматтерах, редакторах и IDE и т.д. В качестве примера можно привести cppcheck, который поддерживает различные стандарты языка Си вплоть до C23 (который используется по умолчанию).

Связь с другими стандартами

Если читателю не приходится ориентироваться в разработке на другие стандарты, то, вероятно, уровень его счастья выше среднего по палате. Однако чаще всего это не так. Предлагаю рассмотреть пару примеров.

Наверное уважаемый читатель знаком со стандартами серии POSIX, по крайней мере слышал о них. Стандарт предназначен для разработки переносимых программ, в первую очередь для UNIX подобных операционных систем. А как же POSIX.1 связан со стандартом языка Си? Может быть и нет никакой связи? Оказывается есть. Стандарт IEEE Std 1003.1 в явном виде ссылается на стандарт языка Си. Обладающие наибольшей степенью реализации в разных ОС стандарты POSIX.1–2001, POSIX.1–2004 и POSIX.1–2008 ориентированы на C99, также как и POSIX.1–2017. А вот уже более свежая редакция POSIX.1–2024 ориентирована на C17.

Ещё один довольно известный стандарт для языка Си это MISRA C. Стандарт в первую очередь предназначен для использования во встраиваемых системах в автомобильной индустрии. Также применяется в медицине, связи и аэрокосмической областях. На мой взгляд MISRA C наиболее полно раскрывается при разработке кода для микроконтроллеров, но и в других условиях применения по крайней мере часть правил весьма полезны. Например, у нас отобраны правила, которые мы стараемся соблюдать в любых проектах. Оказывается, что MISRA C тоже явно ориентирован на конкретный стандарт языка Си, что в общем-то логично. MISRA C:2004 ориентирован на C99. MISRA C:2012 тоже ориентирован на C99, но к нему выходило два исправления — в 2016 году MISRA C:2012 — Amendment 1: Additional Security Guidelines и в 2020 году MISRA C:2012 — Amendment 2: Updates for ISO/IEC 9899:2011/18 Core functionality. Второе как раз привносит поддержку C11 и C17. В 2023 году выпущен стандарт MISRA C:2023, в котором собраны исправления и дополнения MISRA C:2012. Стандарт 2023 года тоже ориентирован на C11 и C17.

Аналогичным образом можно рассмотреть и другие стандарты, которые применяются при разработке.

Выбираем стандарт языка Си

Была рассмотрена краткая история стандартов языка Си от «первобытного общества» до «новейшего времени» и проведена оценка поддержки стандартов языка разными компиляторами и смежными стандартами. Конечно, в каждом конкретном случае могут быть и другие данные для анализа. Однако после их изучения и внимательного рассмотрения надо будет принимать решение.

Итак, попробую подвести итог. Очевидно, что на K&R C ориентироваться в наше время не стоит, вряд ли найдётся современный компилятор, который поддерживает этот стандарт де-факто. ANSI C или иначе C89 уже более реалистичный выбор. Действительно этот стандарт языка поддерживается наиболее полно разными компиляторами и их версиями. C89 часто выбирают проекты, которые рассчитывают на использование на самом широком спектре платформ, в качестве примера можно привести curl. Если же нет таких наполеоновских планов по захвату мира, то стандарт C99 выглядит предпочтительнее. Этот стандарт довольно хорошо поддерживается компиляторами и включает в себя интересные возможности без которых современный Си выглядит непривычно. Если же вопрос портирования в проекте не стоит остро или вовсе отсутствует, то почему бы не сориентироваться на C11, всё-таки многопоточность на уровне языка и атомарность могут оказаться полезными. А если ориентироваться на C11, то почему бы не на его уточнённую версию C17? А может быть вести разработку на C23, ведь в этом стандарте довольно много новых интересных возможностей? Правда, C23 не так давно был опубликован и ещё не учтён в смежных стандартах. Наверное, читатель уже догадался, что мы в команде остановили свой выбор на C17, как наиболее свежем стандарте с неплохой полнотой поддержки.

Буду рад узнать мнения читателей, стоял ли перед ними подобный выбор, как они его решили, на что опирались в своём решении? Обсуждение, уточнения и предложения приветствуются. Добро пожаловать в комментарии!

© Habrahabr.ru