Языку C уже почти 50 лет, но он всё равно крут

Когда раз­го­вор захо­дит о про­грам­ми­ро­ва­нии, мно­гие вспо­ми­на­ют про язык C и про то, какие фан­та­сти­че­ские воз­мож­но­сти есть у тех, кто уме­ет на нём про­грам­ми­ро­вать. Давай­те раз­бе­рём­ся, что же тако­го осо­бен­но­го в этом язы­ке и поче­му про­грам­ми­ро­вать на C слож­нее, чем на совре­мен­ных языках.

Немного важной истории

Сам язык C при­ду­мал Ден­нис Рит­чи в 1972 году — в то вре­мя было чёт­кое раз­де­ле­ние язы­ков программирования:

  • ассем­бле­ры — для систем­но­го про­грам­ми­ро­ва­ния, опе­ра­ци­он­ных систем и драйверов;
  • язы­ки высо­ко­го уров­ня для осталь­ных при­клад­ных задач.

Рит­чи захо­тел сде­лать такой язык, что­бы он по сути был мак­си­маль­но при­бли­жен к ассем­бле­рам по эффек­тив­но­сти и быст­ро­дей­ствию, но имел в то же вре­мя адек­ват­ный и понят­ный чело­ве­ку синтаксис.

У него полу­чил­ся эффек­тив­ный и мощ­ный язык, кото­рый на то вре­мя полу­чил луч­шее от двух миров: ско­рость и мощь ассем­бле­ра и понят­ный син­так­сис язы­ков высо­ко­го уров­ня. Даже почти 50 лет спу­стя C — самый быст­рый и эффек­тив­ный язык для систем­но­го программирования.

В чём сила языка С

На C мож­но напи­сать про­грам­му, кото­рая смо­жет дотя­нуть­ся до любо­го желе­за на ком­пью­те­ре и исполь­зо­вать его в сво­их целях. Вы полу­ча­е­те пол­ный кон­троль над всей памя­тью и аппа­рат­ным устрой­ством компьютера.

Вот что дела­ет C таким мощ­ным языком:

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

Полу­ча­ет­ся, что на C мож­но напи­сать про­грам­му, кото­рая может делать с желе­зом, памя­тью и про­цес­со­ром что угод­но, если в самом коде нет син­так­си­че­ских оши­бок. Но это часто обо­ра­чи­ва­ет­ся мину­са­ми, о них сей­час и поговорим.

Минусы программирования на С

Если бы всё было так про­сто, у C не было бы репу­та­ции одно­го из слож­ных язы­ков про­грам­ми­ро­ва­ния. Шту­ка в том, что все мину­сы C — это обрат­ная сто­ро­на его плю­сов. Смот­ри­те сами:

  • Так как ком­пи­ля­тор сле­дит толь­ко за самы­ми гру­бы­ми ошиб­ка­ми в син­так­си­се, то ему неваж­но, что про­ис­хо­дит с рабо­чей памя­тью. Из-за это­го лег­ко обра­тить­ся не к той обла­сти памя­ти. В луч­шем слу­чае это нару­шит рабо­ту самой про­грам­мы, а в худ­шем — сло­ма­ет дру­гие про­грам­мы и опе­ра­ци­он­ную систе­му (память-то на всех общая).
  • Нет встро­ен­ных средств очист­ки памя­ти, всё нуж­но делать вруч­ную. Если про это забыть, про­грам­ма может занять всю сво­бод­ную опе­ра­тив­ную память.
  • Изна­чаль­ная ори­ен­ти­ро­ван­ность толь­ко на про­це­дур­ное про­грам­ми­ро­ва­ние не поз­во­ля­ет реа­ли­зо­вать ООП-подход в про­грам­мах. Есть спо­со­бы обой­ти это огра­ни­че­ние через ука­за­те­ли, но это слож­ные конструкции.

По сути, един­ствен­ная основ­ная совре­мен­ная пре­тен­зия к C в том, что там нет пол­но­цен­ной реа­ли­за­ции ООП. Все осталь­ные мину­сы — побоч­ный эффект того, что про­грам­мист дол­жен сам преду­смат­ри­вать все нештат­ные ситу­а­ции и сле­дить за памя­тью и пове­де­ни­ем программы.

👉 Мож­но про­ве­сти такую ана­ло­гию: про­грам­ми­ро­вать на C — это как ездить на гоноч­ном боли­де Фор­му­лы 1. Это очень мощ­но, быст­ро и эффект­но, но без опы­та и абсо­лют­но­го зна­ния о том, как управ­лять этим боли­дом, пилот сра­зу выле­тит за ограж­де­ние. Меха­ни­ка вме­сто авто­ма­та, отсут­ствие умных помощ­ни­ков типа кур­со­вой устой­чи­во­сти и помо­щи при зано­сах — всё вруч­ную, но вза­мен пилот полу­ча­ет абсо­лют­ный кон­троль над маши­ной и ста­вит рекорды.

Синтаксис — коротко

Ком­мен­та­рии. Клас­си­ка — делать их вот так:

/* это комментарий */

Но с 1999 года мож­но оформ­лять их с нача­ла строки:

// это тоже комментарий

Фигур­ные скоб­ки. Это важ­ная часть язы­ка — внут­ри таких ско­бок язык вос­при­ни­ма­ет коман­ды как одно целое. Ими зада­ют­ся гра­ни­цы в цик­лах, функ­ци­ях и в любых местах, где нуж­но сгруп­пи­ро­вать несколь­ко команд.

Мате­ма­ти­че­ские и логи­че­ские опе­ра­то­ры. Тут всё стан­дарт­но и выгля­дит точ­но так же, как в Python или JavaScript (кото­рые как раз и поза­им­ство­ва­ли мно­гое из С). Но в C спи­сок таких опе­ра­то­ров намно­го шире: здесь есть и пораз­ряд­ные опе­ра­то­ры, и поби­то­вые, и даже тер­нар­ный опе­ра­тор ? : (это крат­кая фор­ма услов­но­го оператора).

Услов­ные опе­ра­то­ры. Самый про­стой — клас­си­че­ский if:

if ((условие)) (команда_1) else (команда_1)

Ещё есть мно­же­ствен­ный услов­ный опе­ра­тор switch, кото­рый поз­во­ля­ет выби­рать несколь­ко воз­мож­ных зна­че­ний по условию.

Цик­лы. C ними всё про­сто — обыч­ные while-do, do-while и for. Сюр­при­зов в опи­са­ни­ях нет, про­сто зада­ём усло­вие цик­ла и ста­вим фигур­ные скоб­ки в теле цикла.

Пример программы

Давай­те напи­шем код, кото­рый счи­та­ет сум­му чисел от еди­ни­цы до вве­дён­но­го числа:

#define _CRT_SECURE_NO_WARNINGS // чтобы компилятор не ругался на scanf
#include <stdio.h> // подключаем стандартный модуль ввода-вывода
int main() { // начало главной функции
  int k;  // объявляем целую переменную key
  int i = 1; // сразу объявляем переменную со значением
  int sum = 0; // на старте сумма равна нулю
  printf("k = "); // выводим строчку на экран, но пока без значения суммы
  scanf("%d", &k);   // ждём ввода числа и записываем его в переменную k
  while (i <= k)     // делаем цикл — пока i меньше или равно k
  { // как раз те самые фигурные скобки для тела цикла
    sum = sum + i; // добавляем значение i к сумме
    i++;           // увеличиваем i на 1
  } // закончилось тело цикла
  printf("sum = %d\n", sum); // выводим значение суммы, оно получится на той же строке, что и первая строчка
  getchar(); // ждём нажатия любой клавиши, чтобы пользователь мог успеть прочитать ответ
  return 0; // знак того, что программа завершилась без ошибок
} // конец программы

Где применяется С

Мно­гие дума­ют, что на C пишут толь­ко драй­ве­ра и про­грам­мы для мик­ро­про­цес­со­ров, но область при­ме­не­ния C намно­го шире. Сей­час на C пишут:

  • опе­ра­ци­он­ные систе­мы — боль­шин­ство ОС в мире напи­са­ны на C или С++;
  • драй­ве­ры — то же самое, кон­ку­рен­цию состав­ля­ет толь­ко ассемблер;
  • про­грам­мы для мик­ро­кон­трол­ле­ров, умных гад­же­тов, систем охра­ны и вся­ких датчиков;
  • сер­ви­сы и фраг­мен­ты кода в при­клад­ных про­грам­мах там, где важ­но быстродействие.Например, у вас есть сер­вер, кото­рый рабо­та­ет с базой дан­ных, и вам нуж­но, что­бы он эти дан­ные полу­чал как мож­но быст­рее. В этом слу­чае про­ще реа­ли­зо­вать этот сер­вис на С, и тогда он будет рабо­тать так быст­ро, насколь­ко это вооб­ще возможно.

С чего начать

Если вы ещё учи­тесь в шко­ле (или недав­но закон­чи­ли), почи­тай­те Кон­стан­ти­на Поля­ко­ва — у него целая автор­ская под­бор­ка по обу­че­нию про­грам­ми­ро­ва­нию на С.

«Язык про­грам­ми­ро­ва­ния С» — кни­га от самих авто­ров язы­ка. Клас­си­ка учеб­ной литературы.

Онлайн-компилятор язы­ка C — при­го­дит­ся тем, кто хочет перей­ти от тео­рии к практике.