Pull to refresh
0

Миром движет язык С

Reading time 10 min
Views 57K
Original author: Daniel Angel Muñoz Trejo


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

Операционные системы


Многие проекты, написанные на этом языке, стартовали десятилетия назад. Разработка UNIX началась в 1969 году, а её код был переписан на С в 1972. Изначально ядро ОС было написано на ассемблере. И язык С создавался для того, чтобы переписать ядро на языке более высокого уровня, выполняющем те же задачи с использованием меньшего количества строк кода.

В 1985 году вышла Windows 1.0. Хотя код Windows закрыт, в Microsoft утверждают, что ядро её написано по большей части на С. То есть в основе операционной системы, занимающей около 90% мирового рынка в течение десятилетий, лежит язык С.

Разработка ядра Linux началась в 1991 году, оно также по большей части написано на С. В 1992 году ядро Linux вышло под лицензией GNU и использовалось как часть GNU Operating System. А многие компоненты самой ОС GNU написаны на С, поскольку при разработке проекта использовался этот язык и Lisp. Сегодня 97% суперкомпьютеров из Топ 500 наиболее мощных вычислительных систем мира используют ядро Linux. Не говоря уже о миллионах серверов и персональных компьютеров.

Ядро OS X, как и в случае с двумя вышеперечисленными ОС, также в основном написано на С.

В основе Android, iOS и Windows Phone также лежит С, поскольку эти системы являются адаптациями соответственно Linux, Mac OS и Windows. Так что и миром мобильных устройств управляет этот язык.



Базы данных


Все наиболее популярные СУБД, включая Oracle, MySQL, MS SQL Server и PostgreSQL написаны на С (первые три — на С и С++). Эти СУБД используются по всему миру, в финансовых, правительственных, медиа, телекоммуникационных, образовательных и торговых организациях, в социальных сетях и многих других сферах.



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

3D-видео


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

Встроенные системы


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

  • Автоматическая КПП
  • Система контроля давления в шинах
  • Всевозможные датчики (температуры, уровня масла, уровня топлива и т.д.)
  • Управление зеркалами и сиденьями
  • Бортовой компьютер
  • Антиблокировочная система
  • Система удержания в своей полосе
  • Круиз-контроль
  • Климат-контроль
  • Замки с защитой от детей
  • Центральный замок с брелоком
  • Подогрев сидений
  • Система управления подушками безопасности

В большинстве случаев С используется для программирования автоматов по продаже всевозможных снэков и прочих мелких товаров. Также этот язык массово применяется в кассовых аппаратах, в терминалах пластиковых карт.

Большинство встречающихся нам устройств оснащены встроенными системами, то есть процессором/микроконтроллером с соответствующими прошивками и необходимой элементной обвязкой. Это позволяет в небольшом объёме реализовать быструю обработку алгоритмов (иногда достаточно сложных) и взаимодействие с пользователями. Например, тот же будильник определяет, какую кнопку вы нажали, как долго её удерживаете, и в соответствии с этим выполняет ту или иную процедуру, выводя на экран необходимую информацию. Или антиблокировочная система в автомобиле: за очень короткий промежуток времени нужно определить, произошла ли блокировка колёс после начала торможения, и при необходимости циклично включать и отключать тормоза, предотвращая неконтролируемое скольжение автомобиля.

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



Почему мы всё ещё используем С?


Сегодня есть немало языков, которые в каких-то проектах более эффективны, чем С. В каких-то языках куда больше встроенных библиотек, упрощающих работу с JSON, XML, пользовательскими интерфейсами, веб-страницами, клиентскими запросами, подключениями к БД, мультимедиа и т.д.

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

Портируемость и эффективность


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

Портируемость и эффективность языка также связаны с тем, что «компиляторы, библиотеки и интерпретаторы других языков программирования часто реализуются на С». Основные реализации таких интерпретируемых языков, как Python, Ruby и PHP также написаны на С. Данный язык даже используется компиляторами других языков для взаимодействия с аппаратной составляющей. Например, С используется как посредник для языков Eiffel и Forth. Это означает, что вместо генерации машинного кода для каждой архитектуры компиляторы этих языков генерируют промежуточный С-код, его обрабатывает С-компилятор и уже генерирует машинный код.

Язык С фактически стал «лингва франка» для разработчиков. Как отмечает Алекс Эллэйн, руководитель инженеров в Dropbox и создатель сайта www.cprogramming.com:

Язык С позволяет удобно и понятно выражать различные общие идеи в программировании. Более того, многие вещи, используемые здесь, — например, argc и argv для параметров командной строки, операторы циклов, типы переменных, — применяются и во многих других языках. Так что, зная С, вам будет гораздо проще общаться со специалистами в других языках программирования.

Управление памятью


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

При взаимодействии ПО с аппаратной составляющей происходит выделение памяти для периферийных компонентов компьютерных систем и задач ввода/вывода. Системные приложения должны использовать выделенные для них участки памяти для взаимодействия с «миром». И здесь как нельзя кстати приходятся возможности языка С по управлению произвольным доступом к памяти.

К примеру, микроконтроллер можно запрограммировать так, чтобы байт, расположенный по адресу 0x40008000, отправлялся универсальным асинхронным приёмопередатчиком (UART, стандартный компонент аппаратной составляющей для взаимодействия с периферийными устройствами) каждый раз, когда четвёртому биту адреса 0x40008001 присваивается значение 1. Причём после того, как это значение присвоено, оно обнуляется периферийным устройством.

Вот как выглядит код на С, отправляющий байт через UART:

#define UART_BYTE *(char *)0x40008000 
#define UART_SEND *(volatile char *)0x40008001 |= 0x08 

void send_uart(char byte) 
{ 
   UART_BYTE = byte;    // write byte to 0x40008000 address 
   UART_SEND;           // set bit number 4 of address 0x40008001 
}

Первая строка будет представлена в виде:

*(char *)0x40008000 = byte;

Эта запись говорит компилятору интерпретировать значение 0x40008000 в качестве указателя на char, потом с помощью первого оператора * разыменовать указатель (получить значение по адресу), и наконец присвоить значение byte разыменованному указателю. Иными словами, записать значение переменной byte в память по адресу 0x40008000.

Вторая строка будет представлена в виде:

*(volatile char *)0x40008001 |= 0x08;

Здесь мы выполняем побитовую операцию «ИЛИ» над значением по адресу 0x40008001 и значением 0х08 (00001000 в двоичном представлении, то есть 1 в 4-м бите), и сохраняем результат снова по адресу 0x40008001. То есть присваивается значение четвёртому биту байта, расположенного по адресу 0x40008001. Также декларируется, что значение по этому адресу является изменчивым (волатильным). Компилятор понимает, что оно может быть изменено внешними по отношению к нашему коду процессами, поэтому не делает каких-либо предположений относительно этого значения после завершения записи по этому адресу. В данном случае UART возвращает бит в прежнее состояние сразу после того, как ПО присвоило ему значение.

Эта информация важна для оптимизатора компилятора. Если, например, сделать это внутри цикла for, без указания изменчивости значения, то компилятор может предположить, что оно никогда не меняется после записи, и после завершения первого цикла останавливает исполнение команды.

Детерминированное использование ресурсов


Одной из стандартных возможностей языка, на которую нельзя полагаться в системном программировании, является сборка мусора. Для некоторых встроенных систем недопустимо даже динамическое распределение памяти. Встроенные приложения должны выполняться быстро и оперировать очень ограниченными ресурсами памяти. Чаще всего это системы реального времени, в которых непозволительно делать недетерменированный вызов сборщика мусора. И поскольку динамическое выделение нельзя использовать из-за недостатка памяти, то необходимо применять иные механизмы управления памятью, например, размещать данные по конкретным адресам. Указатели в С позволяют это делать. А языки, которые зависят от динамического выделения памяти и сборщика мусора не могут быть использованы в системах с ограниченными ресурсами.

Размер кода


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

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

Пример на С++:

// Class A declaration. Methods defined somewhere else; 
class A
{
public:
   A();                    // Constructor
   ~A();                   // Destructor (called when the object goes out of scope or is deleted)
   void myMethod();        // Just a method
};

// Class B declaration. Methods defined somewhere else;
class B
{
public:
   B();                    // Constructor
   ~B();                   // Destructor
   void myMethod();        // Just a method
};

// Class C declaration. Methods defined somewhere else;
class C
{
public:
   C();                    // Constructor
   ~C();                   // Destructor
   void myMethod();        // Just a method
};

void myFunction()
{
   A a;                    // Constructor a.A() called. (Checkpoint 1)
   {                       
      B b;                 // Constructor b.B() called. (Checkpoint 2)
      b.myMethod();        //                           (Checkpoint 3)
   }                       // b.~B() destructor called. (Checkpoint 4)
   {                       
      C c;                 // Constructor c.C() called. (Checkpoint 5)
      c.myMethod();        //                           (Checkpoint 6)
   }                       // c.~C() destructor called. (Checkpoint 7)
   a.myMethod();           //                           (Checkpoint 8)
}                          // a.~A() destructor called. (Checkpoint 9)

Методы классов А, B и C задаются где-то ещё (например, в других файлах). Поэтому компилятор не может проанализировать их и понять, выдадут ли они исключения. А значит компилятор должен быть готов к обработке возможных исключений от любых конструкторов, деструкторов или вызовов прочих методов. Вообще, деструкторы не должны выдавать исключения, это очень плохая практика, но пользователь может это сделать. Зато деструкторы могут вызвать функции или методы (явно или неявно), которые выдадут исключение.

Если один из вызовов в myFunction выдаёт исключение, то механизм возврата стека (stack unwinding) должен иметь возможность вызова всех деструкторов для уже созданных объектов. Для проверки «номера контрольной точки» (checkpoint number) вызова, инициировавшего исключение, механизм возврата стека использует адрес возврата последнего вызова функции. Делается это с помощью вспомогательной автогенерируемой функции (нечто вроде справочной таблицы), которая используется для возврата стека в том случае, если исключение выдаётся из тела этой функции:

// Possible autogenerated function
void autogeneratedStackUnwindingFor_myFunction(int checkpoint)
{
   switch (checkpoint)
   {
      // case 1 and 9: do nothing;
      case 3: b.~B(); goto destroyA;                     // jumps to location of destroyA label
      case 6: c.~C();                                    // also goes to destroyA as that is the next line
      destroyA:                                          // label
      case 2: case 4: case 5: case 7: case 8: a.~A();
   }
}

Если исключение выдаётся в контрольных точках 1 и 9, то объекты не нуждаются в уничтожении. Если выдаётся в контрольной точке 3, то необходимо уничтожить B и A. Если в точке 6 — нужно уничтожить C и A. В каждом из этих случаев должен соблюдаться порядок уничтожения. В точках 2, 4, 5,7 и 8 уничтожается только объект A.

Эта вспомогательная функция увеличивает размер кода, что является частью дополнительных расходов свободного пространства, характерным отличием С++ от С. Но для многих встроенных систем это «раздувание» недопустимо. Поэтому компиляторы С++ для встроенных систем часто содержат флаг для отключения исключений. Но их отключение имеет свою цену, поскольку Стандартная Библиотека Шаблонов активно использует исключения для информирования об ошибках. И отказ от исключений требует от разработчиков на С++ определённых навыков в выявлении возможных причин ошибок и поиске багов.

Один из принципов С++: «Ты не платишь за то, что не используешь». В других языках ещё хуже обстоят дела с увеличением размера кода за счет добавления разнообразной полезной функциональности, которую не могут позволить себе встроенные системы. И хотя язык С этими возможностями не обладает, объем кода получается куда меньше.

Почему стόит изучать С


Этот язык не труден в изучении, поэтому вам не придётся лезть вон из кожи. Какие вы получите преимущества, освоив С?

Лингва франка. Как уже упоминалось выше, С является своеобразным «универсальным» языком для разработчиков. Многие реализации новых алгоритмов в книгах и на сайтах предоставляются сперва, либо же исключительно, на С. Это позволяет максимально широко их портировать. При этом некоторые программисты, не знакомые с базовыми концепциями С, испытывают сильные затруднения при «конвертации» С-алгоритмов в другие языки программирования.

Поскольку С — язык старый и широко распространённый, то в сети можно найти практически любые необходимые алгоритмы, написанные на нём. Так что знание С открывает перед разработчиком немало возможностей.

Понимание машины. Зачастую, когда разработчики обсуждают поведение того или иного участка кода или функционала в других языках, то разговор ведётся в «терминах С»: Здесь передаётся только указатель на объект, или он копируется целиком? Возможно ли, что здесь есть какое-либо приведение? И т.д.

При анализе поведения кода высокоуровневого языка мало кто дискутирует в терминологии команд ассемблера. Когда мы обсуждаем действия машины, то обычно говорим (и думаем) на С.



Работа над многими интересными проектами, созданными на С. На этом языке создано много любопытных проектов, от больших СУБД и ядер ОС до маленьких приложений для встроенных систем. Стόит ли отказываться от работы с продуктами, которые вам нравятся, только потому, что вы не изучили старый, компактный, мощный и проверенный временем язык программирования?



Заключение


Миром правят не иллюминаты, а программисты на С.

Не похоже, чтобы старичок С начал сходить со сцены. Его близость к «железу», превосходная портируемость и детерминированное использование ресурсов делают С идеальным выбором для низкоуровневой разработки ОС и встроенного ПО. Универсальность, эффективность и хорошая производительность очень важны при создании приложений, осуществляющих сложные манипуляции с данными, вроде БД или 3D-анимации. Да, существует много языков, которые в каких-то задачах использовать выгоднее, чем С. Но по суммарной совокупности преимуществ у С вряд ли есть конкуренты. По производительности С до сих пор остаётся непревзойдённым.

Мир наполнен устройствами, чьё ПО написано на С. Эти устройства ежедневно используют миллиарды людей. Не надо снисходительно относиться к этому языку: он очень активно используется по сей день и, судя по всему, будет использоваться ещё очень долго в самых разных сферах.
Tags:
Hubs:
+69
Comments 69
Comments Comments 69

Articles

Information

Website
www.nixsolutions.com
Registered
Founded
1994
Employees
1,001–5,000 employees