- BrainTools - https://www.braintools.ru -
Недавно я задумался: Python — не единственный инструмент, которым я хочу оперировать в своих инструментах. Python, понятно, легко освоить и он применяется везде, но язык-то не идеальный! Ресурсов требует много, да и время выполнения не ахти, а учитывая нынешние темные времена… Мне нужно что-то получше. В общем, тут я вздумал попробовать Си.
Стоит отметить, что процесс написания кода на Си пускай и трудоемок, и надо следить за огромным количеством факторов, но этот язык является золотым стандартом производительности, который может переплюнуть разве что ассемблер в руках мастера, чей стаж уходит в десятилетия.
Родился этот язык в 1972 году, когда о таких вещах, как веб-разработка, и речи не шло, а игры тогда были вообще без кода, исключительно аппаратные системы! (Например, игра Pong для компьютера Atari 1972 года)
После своего появления Си стал началом новой эпохи разработки программного обеспечения: пока ассемблер был близок к машинному коду, а Бейсик тогда был неким аналогом сегодняшнего Python, Си знатно задрал лапку под древом разработки инфраструктуры.
Влияние Си на ассемблер:
Ассемблер не был поглощен Си, но потребность [1] в нем заметно снизилась, а его использование резко сократилось. До этого именно на нем писались многие системные программы (и части ОС тоже);
Когда Си предложил все то же самое, но лучше, он начал использоваться разве что там, где Си не справлялся: обработка аппаратных прерываний, работа с привилегированными инструкциями процессора, оптимизация критических мест, и так далее;
Стоит также отметить, что большинство компиляторов Си, такие как GCC или Clang, поддерживают ассемблер, вшитый в код Си. Пример синтаксиса GCC: asm("movl %eax, %ebx");
Влияние Си на Бейсик:
Бейсик был подвержен влиянию Си куда сильнее. Ранние версии Бейсика (особенно интерпретируемые) подвергались критике из-за ряда проблем: обилие GOTO (спагетти-код передает привет), слабую модульность, ограниченные возможности работы с данными…
Под влиянием Си Бейсик преобразился (стал снова великим). В нем появились процедуры и функции, как в Си, блочная структура кода (BEGIN/END, SUB/FUNCTION), локальные переменные и структуры данных (как struct в Си);
Большинство версий Бейсика начали мигрировать в компилируемый формат (например, QuickBASIC). Код начал оптимизироваться на уровне компилятора, а еще появилась возможность создавать исполняемые программы (.exe-файлы);
Бейсик даже начал потакать Си: в некоторых версиях (FreeBASIC, например) он начал поддерживать интеграцию .h-файлов (заголовочные файлы Си) и компиляцию кода в тот же формат, что и Си.
Перенесемся в современность, в которой даже в той же веб-разработке преимущественно сидят Python (FastAPI) и JS — интерпретируемые ЯП, разработка на которых легкая, но при этом инференс довольно ресурсозатратен.
В данный момент, принимая во внимание [2] суровые реалии, только слепоглухонемая бабка из глубин Сибири не слышала о всемирном кризисе ОЗУ, виной которому наш любимый (нет) нейрослоп, который мы каждый день видим в любом соцсети, даже порой на этом же Хабре.
Использование генеративного ИИ пошло не туда: он проложил красную ковровую дорожку лентяям, которые вооружились Sora и пошли брать штурмом видеохостинги. Нагрузка на мощности огромная, даже пользовательская DDR5 идет нарасхват, а облачные сервисы дорожают.
Да и жесткие диски тоже на низком старте (хранить нейрослоп данные для обучения [3] ИИ, тоже надо). Дал бы руку на отсечение, что дальше в расход пойдет вода (для охлаждения систем)… Но не буду этого делать, от греха подальше.
Понятное дело, что это никуда не годится. Все уже массово оптимизируют свой код. Я еще не знаю, как именно это происходит, но я бы в этой ситуации вынес горячие зоны бэкенда в компилируемые языки. Си — один из них. Пусть я и не уверен, что он вытеснит Go и Rust, но в критических точках он самый производительный.
На нем построено множество современных ЯП (Python, C++, Rust). В сотни раз быстрее Python и поедает во столько же раз меньше ресурсов. Любимец микроконтроллеров, ОС и прочих специфичных разновидностей ПО. Си — спаситель человечества.
Найдя на Степике бесплатный курс по Си [4] и скачав C××droid из Google Play (уровень подготовки — бог), я сразу же побежал писать следующий код:
int main()
{
printf("Hello, world!")
}
…и сразу же с порога получил ошибку [6]. Даже три сразу.
Во-первых, я забыл заголовок #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");
}
}
Не рекомендую вникать в код: из-за своих ограниченных технических способностей в синтаксисе Си я решил не рисковать, за что поплатился качеством человеческого восприятия [7] кода. Если бы эту статью писал ИИ, он бы определенно стал размышлять на тему лишения человечности во имя алгоритмов машин, но я не буду этим заниматься.
После этого я столкнулся с еще одной напастью: почему-то 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 строки неизменяемые, но в Си вполне)
И в логике [8] ввода:
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 — это указатель на строковый литерал “…”. Они хранятся в памяти [9] для чтения, их нельзя менять;
&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
Источник [10]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/26071
URLs in this post:
[1] потребность: http://www.braintools.ru/article/9534
[2] внимание: http://www.braintools.ru/article/7595
[3] обучения: http://www.braintools.ru/article/5125
[4] бесплатный курс по Си: https://stepik.org/course/C-%D0%B4%D0%BB%D1%8F-%D0%BD%D0%B0%D1%87%D0%B8%D0%BD%D0%B0%D1%8E%D1%89%D0%B8%D1%85-(%D1%82%D0%B5%D0%BE%D1%80%D0%B8%D1%8F-%D0%B8-%D0%B7%D0%B0%D0%B4%D0%B0%D1%87%D0%B8)/
[5] Image: https://sourcecraft.dev/
[6] ошибку: http://www.braintools.ru/article/4192
[7] восприятия: http://www.braintools.ru/article/7534
[8] логике: http://www.braintools.ru/article/7640
[9] памяти: http://www.braintools.ru/article/4140
[10] Источник: https://habr.com/ru/articles/1002612/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1002612
Нажмите здесь для печати.