Обложка статьи «Для чего хорош Си?»

Для чего хорош Си?

Авторы: Dor Marciano, Мария Багулина

Си применяют в сферах, где важен код, наиболее тесно взаимодействующий с «железом». Под влиянием Си появились языки C++, C#, Java и Objective-C.

Си иногда называют подмножеством C++ или «C++ без классов», но это не совсем верно. Почему это не так, можно узнать в статье про C++.

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

  • доступ к памяти через указатели (особые переменные, в которых хранится адрес объекта);
  • активное использование структур и объединений;
  • чистый стиль программирования (код проще отлаживать, но сложнее писать).

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

Оптимизация участков кода на C++

Объектно-ориентированные возможности C++ часто обходятся дороже, чем «чистый Си», так как расходуют больше ресурсов (в частности оперативной памяти). Поэтому иногда код в стиле Си может быть эффективнее. Если нужно заставить какой-либо алгоритм работать быстрее — используйте процедурный стиль и откажитесь от встроенных инструментов C++ для ООП, например от полиморфизма.

Но если нужна действительно высокая скорость, лучше переписать часть кода на ассемблере.

Информационная безопасность

Сюда относятся сложные хакерские приёмы. Среди них:

  • Использование уязвимостей: переполнения буфера, двойные удаления (повреждения кучи).
  • Инъекция (сокрытие) кода. Если получить доступ к другому процессу, используя уязвимость, то можно спрятать свой код внутри чужого и заставить процесс выполнять его. Теперь скрытый код будет жить в «невинном» процессе, спрятанном от глаз пользователя.
  • Перехват (hooking). Если вы хотите мониторить чьи-то взаимодействия с системой (нажатие клавиш, открытие файлов), вам, как правило, нужно вызывать отслеживающий код всякий раз, когда пользователь что-то делает. Для этого вы заменяете какой-либо фрагмент API операционной системы вашим кодом.

Почти для всех этих применений используется PIC код (position-independent code — код, не зависящий от адреса). Он может выполняться в любом месте памяти, независимо от того, где находится и кто его запустил. У PIC-кода нет доступа к глобальным переменным и таблицам, поэтому C++ для его написания не подойдёт (классам C++ нужны глобальные таблицы для реализации наследования).

Код ядра

Код, который выполняется в режиме ядра (kernel mode) имеет полный доступ к памяти и оборудованию: RAM, GPU, жёсткому диску. В режиме ядра работают:

  • Аппаратные драйверы — здесь без доступа к железу не обойтись. Драйверы являются посредниками между пользовательским кодом (не в режиме ядра) и оборудованием.
  • Ядро операционной системы. На Си, кстати, написано множество ядер ОС, в том числе Unix и Android.

Использовать для всего этого код на C++ почти невозможно, поскольку в режиме ядра нет доступа к тем же глобальным таблицам, о которых говорилось выше. Иногда в режиме ядра тоже необходим PIC код — например для загрузчика (bootloader). Загрузчик — самая первая программа, выполняющаяся при запуске ПК. Биос извлекает её из жёсткого диска, помещает в память и говорит процессору запустить эту часть памяти.

Embedded-разработка

Для программирования встраиваемых систем часто используется как Си, так и C++. Но Си имеет преимущество, поскольку позволяет разрабатывать встроенное ПО при ограниченных ресурсах — например когда у микроконтроллера очень мало RAM. Помимо Си также может пригодиться знание ассемблера (как вариант, ARM-ассемблера) для написания ассемблерных вставок, чтобы ещё больше оптимизировать код и получить доступ к специфичным инструкциям процессора.