Мне показалась очень интересной тема открытая в недавней статье

Собрал в одном большом гайде всё, что хотел бы знать, когда изучал язык C

 Во вступлении там написано:

Очевидный факт: язык C — это основа большого количества современных экосистем программирования. Он обеспечивает фундамент многих операционных систем, базовых библиотек и системных инструментов

Мне кажется полезно все таки обозначить в каком смысле язык C — это основа, и чем он обеспечивает фундамент. Я рад что имею возможность сформулировать что-то в данном направлении не со слов иностранного авторитета, а не базе собственного опыта который, тем не менее, формировался на основе анализа-применения действующих решений-теорий сформулированных во многом иностранными специалистами, но надеюсь с примесью собственной креативности в какой-то степени. Ну и хочется озвучить свой ответ на вопрос: "Когда C — идеальный выбор?", может в интерпретации: "А бывает ли язык Си идеальным выбором?"

По моему, тема очень полезная, хотелось бы увидеть и поддержать ее развитие.


Мне кажется в понимании применения языка С очень важно начать с анализа концепций которые унаследованы и развиты в языке С++ и во многих других языках.

Концепция областей видимости

Концепция областей видимости имен которая была введена и явно оформленна в языке Си. Интересно что даже в современных ассемблерах тоже можно выделить что-то напоминающее концепцию области видимости имен, но она там совершенно не оформлена и не стандартизирована.

В языке Си появились не только ограничители области видимости имен в виде блоков кода в фигурных скобоках, единичных файлов и глобальной области видимости, включающей все файлы исходного кода использованные для получения финального исполняемого файла.

Как известно имена (например переменных) объявленные в файле Си-кода получают глобальную видимость, то есть могут быть использованы (доступны для операций, вызваны) в любом другом файле проекта, конечно при соблюдении концепции явной типизации которая требует явного указания типа сущности заданной этим именем. Но мало кто знает(вспомнит) что в рамках языка Си использовалась абстракция Модулей как прототип классов в С++, как единица компиляции объединяющая в себе данные и их типы и функции для работы с этими данными, как единица компиляции которая обеспечивала инкапсуляцию (без разделения доступа подобно public, protected, private в более современных языках) с помощью единственного модификатора доступа static который в рамках С-модуля выполняет эту функцию ограничения доступа, хотя наверно чаще воспринимается только как модификатор способа аллокации, но его функция зависит от контекста, впрочем это не изменилось при переходе в С++, в С++ были добавлены другие модификаторы.

Как же определены Модули в языке Си? Любой файл для которого генерируется файл с объектным кодом, формально является Модулем, но кроме формального определения надо чтобы программист который пишет код воспринимал каждый файл в этом качестве, в качестве единицы компиляции, которая способна обеспечить определенный уровень инкапсуляции, изоляции и возможность повторного использования, как и в С++ и во всех других языках возможности, которые предоставляет программисту язык, не работают сами по себе если мы не знаем о них или не знакомы с техникой их использования. Также как и в С++ мы можем оформлять код в виде классов при этом совершенно игнорируя нормальные (так скажем) правила такого разделения, также и в языке Си мы можем разделять код по файлам (по Модулям) совершенно игнорируя принципы построения действительно кода поддерживающего Модульность в лучшем смысле этого слова.

Концепция бинарных исполняемых файлов

Концепция бинарных исполняемых файлов (нативных программ) и библиотек, то есть бинарных исполняемых файлов с машинным кодом.

Надо понимать что концепция бинарных исполняемых файлов связана и/или распространяется на формирование понятия Операционной Системы так как сама по себе Операционная система тоже является исполняемым файлом (или набором исполняемых файлов). Мало кто задумывается-вспоминает в нужный момент, что сама Операционная система и ее компоненты (драйвера, утилиты из набора утилит коммандной строки) компилируются С/С++ компилятором и сам С/С++ компилятор фактически является одной из утилит командной строки. После компиляции ядра Операционной системы скомпилированный файл просто заменяет текущий файл-образ ядра и после перезапуска машины будет запущен на исполнение как обновленная Операционная система. В этом смысле С/С++ язык тесно связан с понятием Операционной системы, интерпретатора командной строки, файловой системы. Наверно самым удивительным фактом в этом наборе является то, что сам С/С++ компилятор является программой написанной исходно на языке С, а теперь (современные версии) видимо на языке С++ в основном. При этом компилятор тесно связан с Операционной Системой для которой он генерирует исполняемый код, потому что именно Операционная система определяет формат-структуру бинарного исполняемого файла который она способна запускать на исполнение, Exe-файл из windows не работает под Линукс даже на совершенно идентичной машине, это зависит именно от Операционной системы, а не от железа. Не забываем конечно о том что компилятор не может работать вне операционной системы, хотя тут надо уточнять в рамках какой операционной системы, потому что есть такое понятие как крос-компиляция когда мы компилируем-генерируем исполняемый файл в одной операционной системе для другой операционной системы (аппаратной платформы, программно-аппаратной платформы), но это отдельная очень большая тема.

Ядро операционной системы, а также многие ее компоненты не могут быть написаны не на интерпретируемом языке, не на промежуточном языке (в байт-коде, в НЕ-машинных кодах), просто потому что на этом уровне не может существовать программа которая требует существования другой запущенной программы для своего исполнения (интерпретации).

Концепция компиляции

Концепция компиляции, наверно самая важная концепция которая если не родилась вместе с языком Си то очевидно получила окончательную путевку в жизнь с появлением и утверждением этого языка в системобразующем смысле всей програмной-операционной экосистемы Програмного Обеспечения любых более менее тяжелых программно аппаратных систем начиная от десктопных, а в доисторические времена серверно-терминальных систем, до полноценных супер многоядерных серверных систем. Кажется я забыл про легкие микроконтроллерные системы которые вообще работают вне концепции Операционных Систем, но с ними ситуация даже проще, так как Операционная Система формирует какой никакой слой абстракции для програм которые пользуются ее функцианальностью, микроконтроллерные системы обычно требуют более эффективного управления аппаратными ресурсами, которые (ресурсы) существенно ограничены в таких системах поэтому использованию языка Си зачастую нет альтернатив, более того язык Си (а иногда и С++) используются в таких системах без стандартных библиотек. У меня был пример с использованием на микроконтроллере языка С++ обрезанного до такой степени что мы не могли (и не считали нужным!) даже использовать динамическую аллокацию объектов посредством казалось бы неотъемлемого для С++ оператора new. Дело в том что оператор new в C++ это не только элемент базового синтаксиса, это еще и вопрос наличия стандартной (или системной) библиотеки которая обеспечивает набор стандартных функций динамической аллокации и, соответственно, последующего освобождения памяти, которая тянула за собой кучу не нужной нам функциональности и зависимостей в том применении.

Здесь уместно упомянуть что динамическая аллокация как концепция управления выделением памяти тоже изначально появилась в языке С. Для этого, как часть стандарта языка определена стандартная библиотека stdlib, malloc (наверно, проверяйте) с набором соответствующих функций, или, другими словами, библиотека времени выполнения (английская абревиатура CRT),это библиотека которая существует в Операционной системе и функции которой доступны любому скомпилированному исполняемому файлу (вообще говоря из любого языка который поддерживает определенный формат скомпилированных файлов) например в режиме динамической загрузки, то есть когда бинарный код из файла библиотеки загружается как часть исполняемого кода в момент когда этот исполняемый код после своего запуска (в рантайме) в первый раз обращается к функциям этой библиотеки или непосредственно вызывает функции загрузки динамической библиотеки с помощью другого системного АПИ (который-АПИ вообще говоря определен и имеет реализацию в другой динамической или статической библиотеке... Если вам интересно как прерывается эта вообще говоря рекурсия, вы можете найти подходящий более содержательный источник информации, в какой-то степени я пишу такие сложные вещи не для того чтобы вы мне слепо верили, а для того чтобы обозначить направление, заинтересовать, заставить искать подробную информацию! Некоторые глобальные концепции и особенно детали их реализации просто не получится держать в голове постоянно в полном объеме, но полезно представлять себе некоторую общую логику, которая позволит быстро найти нужную детальную информацию когда это действительно нужно).

Вы наверно заметили что я зачастую не разделяю языки С и С++, я тоже это обнаружил с некоторым удивлением, и мне пришлось объяснить это даже себе. Я это делаю не нарочно, дело в том что их (С/С++) не всегда различает компилятор. Насколько я знаю, не существует современных компиляторов которые были бы предназначенны только для С или только для С++. В каком то смысле синтаксис языка С является подмножеством из синтаксиса языка С++ то есть вы даже можете скомпилировать код написанный на чистом С как С++ программу, я правда сам никогда так не эксперементировал хотя занимался симуляцией ембедед Си-кода на десктопных системах, но может кому то покажется интересной идея такого эксперимента. Поэтому когда речь заходит о компиляции и компиляторах очень трудно разделить один язык от другого. Мне кажется такое намеренное разделение в этих случаях очень перегружало бы повествование, поэтому я просто оставляю мысли в том виде в котором они меня изначально посетили. Здесь можно заметить что отличается линковка (linking) она же компоновка для Си и С++ объектных файлов, об этом в отдельном параграфе дальше.

Учитытвая то что теперь трудно найти компилятор (а тем более среду разработки) в которой поддерживается язык Си, но недоступен язык С++, я, например, не вижу особой необходимости писать на Си когда есть возможность писать на С++. Но есть случаи когда определенные ограничения со стороны аппаратной системы и ее окружения-инфраструктуры не позволят вам писать ни на чем кроме языка Си. Так вот я не думаю что к таким случаям надо специально готовиться ограничивая себя только возможностями языка Си. Я думаю если вы свободно владеете синтаксисом языка С++ во всех его проявлениях вплоть до сырых указателей, вам не составит труда перейти на чистый Си где это необходимо, дойти до написания процедур прерываний, до манипуляций с регистрами, битовыми полями...

Концепция явной типизации

Концепция явной типизации (описание унифицированных ресурсов (памяти как минимум) из состава аппартного обеспечения и их абстракций) которая включает в себя список операций известных компилятору для работы с этим типом. Введение в оборот не математических операций для работы с переменными (с именованными програмными сущностями), таких как копирование, аллокация, инициализация, присваивание, разного рода индексации, косвенная адресация,... . Язык С заставляет нас обратить внимание что эти операции, мусорные с точки зрения математики, то есть ни как не влияющие на результат в математических построениях, требуют реальных ресурсов (времени и памяти как минимум) в реальных вычислительных системах и поэтому заставляют пересматривать привычную абстрактную классическую математику для практических run-time вычислений.

В языках С/С++ есть такая довольно уникальная штука как форворд-декларации типов и сигнатур функций. Мало кто задумывается над тем что это по сути дополнительная обязанность разработчика в ручную формировать мета-описания програмных сущностей из своего кода. Так называемые header-файлы содержат описания переменных, функций, типов для их ссылочного использования, плюс компилятор проверяет соответствие этих мета-описаний (деклараций) с продублированными описаниями в файлах реализации.

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

Концепция двух этапной сборки:  компиляция и линковка

Даже если вся программа у вас убирается в единственном файле с исходным кодом вам потребуется провести этот файл через два этапа сборки чтобы получить соответствующий исполняемый файл. Возникает вопрос зачем нужны такие сложности? И тут опять придется вспомнить про понятие Операционной системы в рамках которой только и может выполняться (жить) ваш исполняемый файл-ваша программа.  Например если ваша программа осуществляет какой-то консольный вывод скажем через printf(«Hello world») она обращается к функциям и ресурсам Операционной системы. Стандартным способом связывания нашей пользовательской программы с операционной системой (и самым естественным – оптимальным, кстати, способом - не забываем Операционная Система это тоже С/С++ программа) является линковка с системными библиотеками, которые обычно еще и числятся стандартными Си-шными. Кстати линковка как раз и переводится как связывание с английского. Не надо забывать и о том что когда вы запускаете свой исполняемых файл на исполнение в той же командной строке или мышью из Эксплорера, ваша программа сначала загружается в область памяти для исполнения, а потом уже загруженный код принимает стандартный вызов из операционной системы через стандартную функцию main() обычно, которая в данном случае служит интерфейсом активации кода программы.

Коротко сформулировать суть этапов компиляции и линковки можно примерно так:

Компиляция это замена-перевод С/С++ инструкций-конструкций (кода) в машинные инструкции (машине не нужны конструкции, ей достаточно инструкций :), замена локальных имен адресами, то есть числами (машине не нужны имена :) текстовые файлы перекодируются в бинарные файлы машинного (объектного) кода, формируется список внешних имен которые требуют разрешения на этапе линковки.

Линковка (связывание и компоновка) это объединение кода из разных бинарников и разрешение ссылок на адреса из других файлов и из внешних библиотек, то есть привязка имен внешних функций и переменных к адресам этих внешних (extern) функций и переменных (отсюда термин связывание).

Тут надо заметить что линковка для Си и С++ существенно отличается. Дело в том что объекты линковки на С++ гораздо сложнее чем на чистом Си. Для примера рассмотрим формат сигнатуры функции для линковки в Си и в С++. В языке Си в составе сигнатуры присутствует тип возвращаемого значения, имя функции и список типов параметров.

В С++ возможны функции в глобальной области видимости, в рамках namespace, функции члены класса, типы параметров соответственно тоже становятся намного сложнее, все это разнообразие должно каким-то образом кодироваться в объектных файлах, то есть линковка становится намного сложнее.

Возможно поэтому некоторые проекты пишутся на С++, но при этом используют простую Си-шную линковку, для примера можно посмотреть код достаточно известного и большого проекта LibreOffice. Поиск по проекту строки extern "C" выдает более тысячи таких объявлений.

Главная функция системы сборки

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

Как ни странно самая сложная из задач которые решает эта утилита и декларативный (частично) язык который она обрабатывает, обычно упоминается как-то мельком, поэтому хочу попробовать изложить свою более развернутую версию, которая конечно основана на положениях соответствующей нативной документации .

Кроме очевидной задачи запрограмировать выполнение всех этапов-операций необходимых для генерации финального исполняемого файла, система сборки (скрипт сборки) уже после того как сборка была выполнена хотя бы один раз, обязан следить за тем в каких элементах участвующих в сборке произошли изменения с момента последней успешной процедуры сборки, чтобы не выполнять повторно уже выполненные операции. Я вас уверяю что скрипты сборки были бы намного проще если бы им не надо было бы следить за тем, а что из сотен тысяч целевых и промежуточных файлов из состава проекта надо пересобирать, перекодировать, перепроверять, ... Я даже не знаю имеет ли смысл приводить в пример все тот же проект LibreOffice потому что, ну уж очень сложно разобраться как же там работает система сборки, понять идею с построением какталога с зависимостями в файловой системе перед основным построением.

Кстати тут можно обратить внимание как утилита сборки замыкает экосистему разработки программного обеспечения внутри операционной системы. Утилита MAKE не может работать без специальной функциональности файловой системы, которая в свою очередь является штатной функциональностью Операционной системы. Файловая система для каждого файла сохраняет время его последней модификации-изменений. Без возможности доступа к этим данным система сборки не может работать. Операционная система запускается из файла (который обычно сохранен в специальном заданном аппаратно месте) компилятор компилирует Операционную систему и сам себя в рамках ОС, компилятор нужен чтобы добавлять функции-операции в ОС, все они вместе распологаются в файловой системе, плюс компилятор использует файловую систему и ее специальные функции для создания новых программ и для обновления всего, себя в том числе.

Интересный факт, кстати, что даже проблема Undefined Behavior некоторыми решается на уровне системы сборки или даже на уровне конфигурации системы сборки. Решается фактически в лоб. Где то рядом со скриптами сборки добавляются эталонные файлы Си или С++ кода с проблемными конструкциями кода и добавляется такой предварительный этап сборки на котором эти эталонные файлы компилируются и проверяется или результат компиляции или дело доходит вплоть до анализа сгенерированного ассемблерного кода, и в зависимости от этих результатов выставляются DEFINE-константы которые управляют условной компиляцией уже внутри основного проекта. Это довольно распространенная практика в open-source кстати.

На этом пока все! Устал умничать :). Пишите в чем ошибся, чего не так понял, что пропустил.

Комментарии (3)


  1. yamakasy267
    10.01.2025 09:06

    Насколько я знаю, не существует современных компиляторов которые были бы предназначенны только для С

    Ну я нашел компилятор TCC и даже нашел достаточно интересную статейку с его использованием) Но в целом, конечно, сложно найти компилятор, который поддерживает компиляцию только Си или плюсов. Я думаю это обусловлено скорее удобством, что бы был один инструмент. Потому, что в целом я не редко встречал, когда эти универсальные компиляторы использовали, но с флагами, которые определяют компиляцию например только для Си.

    Я думаю если вы свободно владеете синтаксисом языка С++ во всех его проявлениях вплоть до сырых указателей, вам не составит труда перейти на чистый Си где это необходимо...

    Честно, я так и не примирился с этой идеологией)) Очень часто я встречал данную мысль и так и не смог с ней ужиться. Возможно, что просто еще не достаточно продвинут в плюсах, но каждый раз при виде чистого Си впадаю в ступор) Мой скромный опыт показал, что границы между Си и С++ стерты(в хорошем смысле) у программистов, которые начинали с Си и перешли на С++ , либо те кто начинал примерно с С++ 98, вот они достаточно ловко переключаются между Си и плюсами. Я пришел уже в С++ 17 и судя по тому, что те кто со мной тогда обучался, так же так и не смогли принять Си, видимо подтверждает теорию, что к хорошему привыкаешь быстро) Тогда же кст видел, что многие не разделяют Си и С++ уже в плохом смысле, не понимая какая функция является сишной, а какая плюсовой. Поэтому, конечно хорошо, что очень опытные программисты умеют стирать границы между этими языками, но мне кажется , что пора уже разделить их


    1. rukhi7 Автор
      10.01.2025 09:06

      но мне кажется , что пора уже разделить их

      так а что значит разделить? убрать из С++ синтаксис чистого Си? Я думаю что такие прецеденты уже есть, например Java, вполне можно рассматривать как подобного рода эксперимент, как вы думаете? Ну там прикрутили еще сборщик мусора конечно, но основное то - классы, интерфейсы, дженерики - идеи те же что в плюсах в общем и целом. Как минимум можно анализировать что получается если С++ оторвать от исходного Си синтаксиса и связанных с ним возможностей на примере, по моему.


      1. yamakasy267
        10.01.2025 09:06

        например Java, вполне можно рассматривать как подобного рода эксперимент, как вы думаете?

        Да , вполне. И хотя я являюсь большим не любителем Java, с цифрами спорить сложно, а значит в каком то роде эксперимент удачный, разве нет?

        Но вообще, я скорее имел ввиду прежде всего какого то морального и ментального разделения дынных языков. Ведь не даром когда идет разговор об одном из них, то вроде как подразумевают и другой. И пишут всегда как C/C++ собственно и используют как я уже писал в перемешку(там где нужно и не нужно). И хотя , насколько я знаю, сам Страуструп закладывал в язык философию расширения языка Си и что конструкции в обоих языках должны быть равнозначны и т.д. Но поему С++ уже перерос это. Причем я вижу, как компании стараются переходить только на С++(даже микроконтроллеры , некоторые, пишут уже только на С++), но как будто С++ , на современной стадии развития, не хватает самостоятельной идеи, что ли. Просто я помню спрашивал у профессоров у себя в универе, и на вопрос, а в чем смысл С++ и его собственно плюсы) Получал ответы сводящиеся: "Синтетический сахар". И это грустно, хотелось бы четкого разделения, что вот тут мы используем Си(если он так необходим) потому что он... , а вот тут именно С++ потому, что он... И я имею ввиду не части одной программы, а именно разные проекты и подходы разработки.

        P.S Да я понимаю, что вроде как идет неспешный переход от Си к С++. И все это нужно ибо легаси код на Си не выкинешь в мусор и он должен работать, пока в проект постепенно внедряется С++.(Вроде как раз в новом Unreal 5 компания практически закончила переход). Но как будто переход затянулся и пора все таки разъединяться

        P.S.S. Пока писал решил немного погуглить и даже нашел вот такой раздел в вики С++. Не знаю наскок он имеет место быть в жизни, но если это правда, то вроде как подтверждает мои мысли о том, что С++ уже перерос как усиление Си и пора ему обрести собственный смысл существования и отделиться от Си