Привет, Хабр! Недавно я задумался: 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("%s\n", 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(), как ни странно, тоже показались мне гораздо дружественнее (никогда такого не было, чтобы я ошибся в указателях с ними. Хоть убейте — не было!). Пусть функции с массивами позже и заставили меня взвыть, но в целом Си мне понравился.
Ожидаю фидбека в комментариях!
