Недавно я задумался: Python — не единственный инструмент, которым я хочу оперировать в своих инструментах. Python, понятно, легко освоить и он применяется везде, но язык-то не идеальный! Ресурсов требует много, да и время выполнения не ахти, а учитывая нынешние темные времена… Мне нужно что-то получше. В общем, тут я вздумал попробовать Си.
Как Си спас инфраструктуру человечества и сделает это еще раз
Стоит отметить, что процесс написания кода на Си пускай и трудоемок, и надо следить за огромным количеством факторов, но этот язык является золотым стандартом производительности, который может переплюнуть разве что ассемблер в руках мастера, чей стаж уходит в десятилетия.
Родился этот язык в 1972 году, когда о таких вещах, как веб-разработка, и речи не шло, а игры тогда были вообще без кода, исключительно аппаратные системы! (Например, игра Pong для компьютера Atari 1972 года)
После своего появления Си стал началом новой эпохи разработки программного обеспечения: пока ассемблер был близок к машинному коду, а Бейсик тогда был неким аналогом сегодняшнего Python, Си знатно задрал лапку под древом разработки инфраструктуры.
Влияние Си на ассемблер:
-
Ассемблер не был поглощен Си, но потребность в нем заметно снизилась, а его использование резко сократилось. До этого именно на нем писались многие системные программы (и части ОС тоже);
-
Когда Си предложил все то же самое, но лучше, он начал использоваться разве что там, где Си не справлялся: обработка аппаратных прерываний, работа с привилегированными инструкциями процессора, оптимизация критических мест, и так далее;
-
Стоит также отметить, что большинство компиляторов Си, такие как GCC или Clang, поддерживают ассемблер, вшитый в код Си. Пример синтаксиса GCC:
asm("movl %eax, %ebx");
Влияние Си на Бейсик:
-
Бейсик был подвержен влиянию Си куда сильнее. Ранние версии Бейсика (особенно интерпретируемые) подвергались критике из-за ряда проблем: обилие GOTO (спагетти-код передает привет), слабую модульность, ограниченные возможности работы с данными…
-
Под влиянием Си Бейсик преобразился (стал снова великим). В нем появились процедуры и функции, как в Си, блочная структура кода (BEGIN/END, SUB/FUNCTION), локальные переменные и структуры данных (как struct в Си);
-
Большинство версий Бейсика начали мигрировать в компилируемый формат (например, QuickBASIC). Код начал оптимизироваться на уровне компилятора, а еще появилась возможность создавать исполняемые программы (.exe-файлы);
-
Бейсик даже начал потакать Си: в некоторых версиях (FreeBASIC, например) он начал поддерживать интеграцию .h-файлов (заголовочные файлы Си) и компиляцию кода в тот же формат, что и Си.
Время лихое, но для умелых золотое
Перенесемся в современность, в которой даже в той же веб-разработке преимущественно сидят Python (FastAPI) и JS — интерпретируемые ЯП, разработка на которых легкая, но при этом инференс довольно ресурсозатратен.
В данный момент, принимая во внимание суровые реалии, только слепоглухонемая бабка из глубин Сибири не слышала о всемирном кризисе ОЗУ, виной которому наш любимый (нет) нейрослоп, который мы каждый день видим в любом соцсети, даже порой на этом же Хабре.
Использование генеративного ИИ пошло не туда: он проложил красную ковровую дорожку лентяям, которые вооружились Sora и пошли брать штурмом видеохостинги. Нагрузка на мощности огромная, даже пользовательская DDR5 идет нарасхват, а облачные сервисы дорожают.
Да и жесткие диски тоже на низком старте (хранить нейрослоп данные для обучения ИИ, тоже надо). Дал бы руку на отсечение, что дальше в расход пойдет вода (для охлаждения систем)… Но не буду этого делать, от греха подальше.
Понятное дело, что это никуда не годится. Все уже массово оптимизируют свой код. Я еще не знаю, как именно это происходит, но я бы в этой ситуации вынес горячие зоны бэкенда в компилируемые языки. Си — один из них. Пусть я и не уверен, что он вытеснит Go и Rust, но в критических точках он самый производительный.
На нем построено множество современных ЯП (Python, C++, Rust). В сотни раз быстрее Python и поедает во столько же раз меньше ресурсов. Любимец микроконтроллеров, ОС и прочих специфичных разновидностей ПО. Си — спаситель человечества.
Мой личный опыт с Си
Найдя на Степике бесплатный курс по Си и скачав C××droid из Google Play (уровень подготовки — бог), я сразу же побежал писать следующий код:
int main()
{
printf("Hello, world!")
}
…и сразу же с порога получил ошибку. Даже три сразу.
-
Во-первых, я забыл заголовок #include <stdio.h>. Она содержит самые базовые функции ввода и вывода;
-
Во-вторых, после каждой строки кода должна быть точка с запятой (“;”), мол, “начало мысли” -> “конец мысли”. Как в JS, но тут это обязательно;
-
В-третьих, желательно в функции main() в конце приписывать return 0;. Ошибка более стилистическая для новых компиляторов, но если этого не сделать, то в некоторых случаях вы даже не будете знать, когда программа завершится. Возврат нуля здесь является чем-то вроде выражения “уйти с миром”.
После радушных объятий компилятора и анализа своих ошибок мой код выглядел так:
#include <stdio.h>
int main()
{
printf("Hello, world!");
return 0;
}
На Python для этого мне бы понадобилась одна строка! Зато вместо миллисекунд Python (конкретно для этого случая) у меня на вывод ушло, ну… Гораздо меньше времени. Вычитая компиляцию в бинарник.
После часа изучения курса по Си я осмелел и даже написал что-то в духе:
#include <stdio.h>
int main()
{
int x = 7;
int y = 2;
printf("%f", (float)x / (float)y); // вывод: 3.500000
return 0;
}
Код объявляет две целочисленных переменных: x (7) и y (2). А затем делит их между собой (до этого переведя их в формат типа 7.0 и 2.0) и получает 3.500000 на выводе.
Небольшое пояснение: в Си при делении целых чисел 7 / 2 получится 3. Деление двух целых чисел работает здесь как оператор // в Python. Если вы хотите получить конкретный результат, то переводите числа в float или double.
Возмужавши от своих подвигов на курсе, я решил слегка выйти за его рамки и написать что-то свое. Например, те же “камень-ножницы-бумагу”. Особенно после того, как я узнал, что в stdlib.h есть функция rand(), которая, впрочем, работает чуть иначе, чем в Python. Но об этом осознании чуть попозже.
Веселая нарезка (страданий) моего мозга
Прототип на Python выглядел бы примерно так:
import random
plays = ["rock", "paper", "scissors"]
# Обработка ввода игрока
p_choice = input()
while p_choice != "q": # выход при вводе q
player = player.lower() # перевод всего ввода в нижний регистр
c_choice = random.choice(plays) # выбор компьютера
# Логика сравнения
if p_choice == c_choice:
print("Draw!")
elif p_choice == "rock":
if c_choice == "scissors":
print("You won!")
else:
print("Computer won!")
elif p_choice == "scissors":
if c_choice == "paper":
print("You won!")
else:
print("Computer won!")
elif p_choice == "paper":
if c_choice == "rock":
print("You won!")
else:
print("Computer won!")
print() # пропустить строку
p_choice = input()
На Си это оказалось гораздо сложнее. Мне пришлось столкнуться с:
-
указателями строк;
-
сегфолтами;
-
определением функций (и я не про int main);
-
массивами;
-
условиями;
-
и прочими буками Си

Начнем с того, что при попытке объявить массив я написал:
char arr[3] = {"rock", "paper", "scissors"};
…НЕТ. То есмь массив из 3 символов, а я пытался запихнуть туда строки. Надо делать либо:
char *arr[3] = {"rock", "paper", "scissors"};
// массив указателей
Либо:
char arr[3][10] = {"rock", "paper", "scissors"};
// двумерный массив из 3 строк по 10 символов
// этот способ мне не понравился, так что я взял первый
Потом я принялся реализовывать рандом. Избалованный Python, я попробовал функцию rand() из stdlib.h:
#include <stdio.h>
#include <stdlib.h>
int main()
{
char *plays[3] = {"rock", "paper", "scissors"};
int count = sizeof(plays) / sizeof(plays[0]);
int index = rand() % count;
printf("%s", plays[index]);
return 0;
}
Вот только чего-то не хватает. Почему-то мне всегда выпала только “бумага”: я не задал семя рандома (srand). Для исправления сего недоразумения мне пришлось сделать так:
#include <stdio.h>
#include <stdlib.h>
#include <time.h> // нужно для time()
int main() {
srand(time(NULL)); // устанавливаем яблоко рандома
char *plays[3] = {"rock", "paper", "scissors"};
int count = sizeof(plays) / sizeof(plays[0]);
int index = rand() % count;
printf("%sn", plays[index]);
return 0;
}
Теперь рандом зависит от того времени, когда программа выполняется!.. И все же можно сделать лучше.
#include <unistd.h>
unsigned int seed = time(NULL) ^ getpid();
srand(seed); // устанавливаем яблоко рандома (но лучше!)
Выглядит это так:
-
мы получаем текущее время на момент запуска;
-
а также PID операции (все PIDы разные в рамках одной операции ОС);
-
побитовое XOR (^) комбинирует оба источника энтропии, усиливая перемешку;
Теперь наш генератор работает горвздо лучше!
Позже, когда я добрался до написания условий, я решил посмотреть, как сравнивать строки в Си, и не зря. Тут не прокатит прямое сравнение, как в Python: в Си для этого существует функция strcmp(str1, str2), возвращающая число, равное:
-
0, если они равны;
-
-1, если первый ASCII-символ первой строки меньше второго;
-
1, если наоборот
Прозвучало страшно, но я быстро понял, как это использовать. Я создал условие вида if (strcmp(p_choice, “scissors”) == 0), которое определит, ввел ли игрок “scissors”. Сам же алгоритм определения выглядит так:
#include <stdlib.h>
#include <string.h>
int main()
{
if (strcmp(c_choice, p_choice) == 0)
{
printf("nDraw!n");
}
if (strcmp(c_choice, "rock") == 0 && strcmp(p_choice, "paper") == 0 || strcmp(c_choice, "scissors") == 0 && strcmp(p_choice, "rock") == 0 || strcmp(c_choice, "paper") == 0 && strcmp(p_choice, "scissors") == 0) // условия победы игрока
{
printf("nPlayer won!n");
}
if (strcmp(c_choice, "paper") == 0 && strcmp(p_choice, "rock") == 0 || strcmp(c_choice, "scissors") == 0 && strcmp(p_choice, "paper") == 0 || strcmp(c_choice, "rock") == 0 && strcmp(p_choice, "scissors") == 0) // условия победы компьютера
{
printf("nComputer won!n");
}
}
Не рекомендую вникать в код: из-за своих ограниченных технических способностей в синтаксисе Си я решил не рисковать, за что поплатился качеством человеческого восприятия кода. Если бы эту статью писал ИИ, он бы определенно стал размышлять на тему лишения человечности во имя алгоритмов машин, но я не буду этим заниматься.
После этого я столкнулся с еще одной напастью: почему-то if игнорировал мой ввод! Я не знал, как это произошло (я все же вводил “paper”, я искренне не понимаю, как это случилось), но на всякий случай я решил определить функцию, переводящую ввод в нижний регистр.
Видите ли, у Си нет прямого аналога питоновского .lower(). Есть только tolower() из ctype.h, но он охватывает только один символ, а потому мне пришлось объявить первую Си-функцию, которая НЕ является main():
#include <ctype.h>
// перевод в нижний регистр
void lower(char *str) {
for (int i = 0; str[i]; i++) {
str[i] = tolower(str[i]);
}
Я буду честен: я больше скопировал эту функцию, чем написал ее. Все же я вник, и понял, как она работает:
-
void— наша функция ничего не возвращает (intпередmain()означает, чтоmain()вернет число); -
char *str— указатель на строку; -
forздесь, в принципе, остался в том же амплуа, что и в Python, разве что замаксировался иначе; -
int i = 0— в тандеме сforиi++образует выражение вида питоновскогоfor i in range; -
str[i]— как в Python, берет индекс буквы в строке; -
str[i]=tolower(str[i])— меняет символ на этот же символ нижнего регистра (в Python строки неизменяемые, но в Си вполне)
И в логике ввода:
int main()
{
printf("Enter your choice:t");
scanf("%s", p_choice);
lower(p_choice); // теперь наш ввод в нижнем регистре
printf("Computer picked %s", c_choice);
printf("nYour input: %s", p_choice);
}
Неизвестно почему, но опосля определения функции все заработало.
Кстати, о вводе:
char *p_choice = "...";
printf("Введите выбор: ");
scanf("%s", &p_choice);
Никогда так не делайте. Тут дорога только в сегфолт (Segmentation fault).
-
p_choice— это указатель на строковый литерал “…”. Они хранятся в памяти для чтения, их нельзя менять; -
&p_choiceпередавал адрес указателя, а не адрес буфера для строки; -
Введенная строка летит в память чтения — поздравляем, вы выиграли “Segmentation fault”!
Лично я решил проблему, изменив секцию на оную типа:
char p_choice[100]; // буфер для ввода в 100 символов
printf("Введите выбор:t");
scanf("%s", p_choice); // без &, потому что массив уже адрес
Вконец уставший от сложностей Си, я почти закончил алгоритм. Все, что мне осталось — это сделать цикл while, потому что неудобно запускать программу сначала каждый раз. Но с этим проблем уже не возникло:
while (strcmp("q", p_choice) != 0)
{
// ...логика игры...
// ...тут же и логика ввода пользователя в конце...
}
if (strcmp("q", p_choice) == 0) //окончание игры
{
printf("nQuitting...n");
}
После тестирования я выдохнул с облегчением. Наконец, я закончил с логикой этой игры, полностью переведя ее с Python на Си. И если в Python мне бы понадобилось 25 строк, то на Си мне понадобилось 45 строк. Большинство из них — фигурные скобки на отдельной строке, да и код кое-где можно было бы оптимизировать, но эта задача пока не для меня.
Моя реализация Си-кода выглядит так:
// #include <stdio.h> в принципе, можно исключить во имя stdlib.h
#include <stdlib.h>
#include <time.h>
#include <ctype.h>
#include <unistd.h>
#include <string.h>
// перевод в нижний регистр
void lower(char *str) {
for (int i = 0; str[i]; i++) {
str[i] = tolower(str[i]);
}
}
int main()
{
unsigned int seed = time(NULL) ^ getpid();
srand(seed); // устанавливаем яблоко рандома
char *plays[3] = {"rock", "scissors", "paper"};
int count = sizeof(plays) / sizeof(plays[0]);
int index = rand() % count;
char *c_choice = plays[index]; // рандомный выбор
char p_choice[100];
while (strcmp("q", p_choice) != 0)
{
printf("Enter your choice:t");
scanf("%s", p_choice);
lower(p_choice);
printf("Computer picked %s", c_choice);
printf("nYour input: %s", p_choice);
if (strcmp(c_choice, p_choice) == 0)
{
printf("nDraw!n");
}
if (strcmp(c_choice, "rock") == 0 && strcmp(p_choice, "paper") == 0 || strcmp(c_choice, "scissors") == 0 && strcmp(p_choice, "rock") == 0 || strcmp(c_choice, "paper") == 0 && strcmp(p_choice, "scissors") == 0) // условия победы игрока
{
printf("nPlayer won!n");
}
if (strcmp(c_choice, "paper") == 0 && strcmp(p_choice, "rock") == 0 || strcmp(c_choice, "scissors") == 0 && strcmp(p_choice, "paper") == 0 || strcmp(c_choice, "rock") == 0 && strcmp(p_choice, "scissors") == 0) // условия победы компьютера
{
printf("nComputer won!n");
}
}
// окончание игры
if (strcmp("q", p_choice) == 0)
{
printf("nQuitting...n");
}
return 0;
}
Говорите, что хотите, но я горжусь этим кодом. Написал я его на второй день изучения Си (возможно, мне не стоило заскакивать вперед курса и искать решения багов окаяных), но в конечном итоге у меня получилось.
В конце можно замолвить словечко о том, что учить Си пусть и будет нелегко, но зато код будет быстрым и эффективным. Мне понравилось то, что в этом языке все надо настраивать самому, а это уже по-настоящему максимальный контроль. printf() и scanf(), как ни странно, тоже показались мне гораздо дружественнее (никогда такого не было, чтобы я ошибся в указателях с ними. Хоть убейте — не было!). Пусть функции с массивами позже и заставили меня взвыть, но в целом Си мне понравился.
Ожидаю фидбека в комментариях!
Автор: Garantia_Tsverga


