×

Введение в Си

Часто задают вопрос: "С чего начать изучать IT?". Поэтому выбработался почти стандартный ответ:

  • tasm (old school) / C (new wave) – для понимания, как устроен компьютер.
  • Perl / Python – для понимания скриптовых языков.
  • LISP / Haskell – для понимания функциональной парадигмы.

Ранее C рассматривался в курсе "Языков и Технологий Программирования", но его заменили на C#. Казалось бы, один символ, но вся системшина ушла.

Ну и хорошо! Хорошо же?

Как только мы переходим к реальности: ассемблер, аппаратные средства вычислительной техники... Мы оказываемся в абсолютной тьме. Потому что важно перед тем, как копнуть совсем в железо, надо понимать компьютер хоть на уровне околосистемного программиста.

Именно так и вернулся Си в программу.

Ориентируемся на стандарт С11, компилятор clang.

Продолжительность

Курс длится 1 семестр, 1 пара в неделю. Лекции и практика совмещены.

Отчётность

Допуск от 60 баллов, зачёт.

История

Язык Си разработан Деннисом Ритчи (помните, он ещё и Unix сделал) в соавторстве с Брайаном Керниганом. Всё это было в лабораториях Белла (AT&T). А всё из-за отсутствия вменяемого инструмента разработки системных программ – по сути только одих ассемблер был... Хотелось чего-то более высокоуровневого.

Где используется Си, мотивация изучать Си

Unix написан на Си, Linux написан на Си... Драйверы на Си. Даже Windows на Си. Всё это – реальное положение дел в компьютере.

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

То есть по большей части Си используется для: - Разработки Операционных Систем (системное/сетевое программирование) - Программирования микроконтроллеров (носимая электронника, машины, кофеварки и тд) - ПО реального времени (стримминг/перекодировка видео/аудио) - Если нужно хоть как-то взаимодействовать с железом - придётся написать биндинг для вашего любимого языка на... Си. Такие дела.

Что есть в языке Си?

Мало ключевых слов – легко выучить ;-)

Вместо классов (описатетелей сложных сущностей) – структуры данных (+ юнионы). Аналог структур в Python.

Мощный инструмент указателей (на память) – помните принцип адресуемости памяти?

Компилируется в нативный код – читай в язык процессора.

Макросы (препроцессор) – надстройка, позволяющая "именовать" частоповторяющиеся операции.

Чего нет в языке Си?

Проверки границ массивов/структур и вообще данных. Легко можно испортить. Например, мы можем создать массив из 10 элементов и записать в 11-ый... Само собой, это повлияет на другие данные, сломает вашу программу.

Строгая типизация. Типы есть, но они довольно неплохо преобразуются друг к другу. Да и вцелом – мы работаем с памятью, все типы данных – просто условность! И хоть и есть ошибки компиляции, связанные с проверкой типов данных, далеко не всё проверяется.

Автоматической сборки мусора. Всё, что динамически создаёте – сами и удаляете. Не забывайте прибираться – иначе утечки памяти.

Нет исключений (exceptions) – ошибки не райзятся, а обрабатываются на уровне возвращаемых значений / спец. переменной errno.

ООП. На самом деле, были попытки, но Си не про ООП, а про данные и устройства.

Простейшая программа (Пишем утилиту POSIX "false")

Пишем утилиту POSIX "false"

C /* Это первая программа на Си */ int // функция возвращает число (integer) main(void) { // Принимает ничто (void) return 1; // Всё "плохо" - вернули 1 }

Что происходит: - Комментарии могуть быть // и /* */ - Объявляем функцию main – именно она является точкой входа для программы на Си: - Сначала пишем int – функция вернёт знаковое число (integer). - В скобках – аргументы функции. В нашем случае ничего не надо. Ничто = void. - В теле функции (внутри фигурных скобок): - Возвращаем из функции 1. Для функции main – это код возврата команды (errorlevel / $?).

Компиляция (на примитивном уровне), запуск

Собираем компилятором clang:

Bash clang 01-false.c -o ./false

Что происходит: - clang'у для компиляции даём файл 01-false (с программой, описанной выше). - Ключ -o – куда выводить результат. К нашем случае – ./false.

Clang – один из компиляторов Си (также есть gcc, MSVS и т.д.).

Запускаем:

Bash → false → echo $? 1

Функции в первом приближении (ибо main - функция)

В общем виде функцию можно описать следующим шаблоном:

C <возвращаемый тип> <имя функции>(<тип1> <арг1>, <тип1> <арг2>, ...) { <тело функции> }

Функция имеет свою локальную область видимости – переменные, определённые внутри неё недоступны снаружи. По факту они создаются на стеке и после выхода из функции стек откатится к состоянию до попадания в функцию – данные станут недоступны (разве что не найдётся какой-то обходной путь – хак).

Параметры функции передаются по значению и их изменения не будут видны снаружи функции (они также объявляются на стеке). С другой стороны, параметром может быть адрес в памяти – тогда можно модифицировать ту память.

Возврат значений производится, как и во многих языках с помошью return.

Что такое int?

Размер int зависит то платформы. Посмотреть размер тип или переменной можно функцией sizeof:

C int main() { return sizeof(int); }

Собираем:

Bash clang 01-int_size.c -o ./int_size

Запускаем:

Bash → false → echo $? 4

А что, если мы туда положим что-то большее?

C int main(void) { int num = 0xffffffff; return num + 12; }

Переполнение:

Bash → echo $? 11

На самом деле 0xffffffff – не самый большой int, самый большой – 0x7FFFFFFF из-за устройства представления байтов знаковым целым числом. В диапозоне [8000000, 0xffffffff] - отрицательные числа. Однако, 0xffffffff прекрасно справляется с демонстрацией переполнения 0xffffffff + 12 = 11.

Препроцессор Си

Невероятно, но факт -- Ритчи и Керниган придумали язык над языком. Ещё до того, как компилятор начинает собирать код Си, запускается препроцессор, в задачах которого:

  • замена соответствующих диграфов и триграфов на эквивалентные символы «#» и «\»;
  • удаление экранированных символов перевода строки;
  • замена строчных и блочных комментариев пустыми строками (с удалением окружающих пробелов и символов табуляции);
  • вставка (включение) содержимого произвольного файла (#include);
  • макроподстановки (#define);
  • условная компиляция (#if, #ifdef, #elif, #else, #endif);
  • вывод сообщений (#warning, #error).

И скептик бы сказал, что это очередной костыль, чтобы это всё заработало, когда стало большим... Так и есть! Однако именно благодаря препроцессору мы можем подключить заголовочный файл работы с вводом-выводом (stdio.h) и использовать printf. К счастью, тогда костыли были зеленее, а небо голубее...

C #include <stdio.h>

Как раз этой командой препроцессору мы говорим подключить заголовочный файл stdio.h для того, чтобы мы могли использовать функции (определённые в нём). Например, оттуда мы можем получить printf -- уже знакомый вам по курсу ОС. Также там есть функция puts -- положить строку на стандартный вывод.

Препроцессор Си -- одно из средств упростить разработку на Си. Мы часто будем прибегать к нему. И рассмотрим подробнее позднее. А также напишем свои макросы и header файлы.

Hello world

Традиционный (а стал он таким с K&R) стартовый пример кода:

```C #include <stdio.h>

int main(void) {
    puts("Hello world");
    return 0;
}

```

Что происходит: - В теле функции (внутри фигурных скобок): - Кладём строку на стандартный вывод (puts). Параметром идёт строка, которую хотим вывести. - Возвращаем из функции 0. Для функции main – это код возврата команды (errorlevel / $?).

Собираем, запускаем:

Bash → clang 01-hello-world.c -o hello_world → ./hello_world Hello world

Арифметика

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

```C

include <stdio.h>

int main(void) { int a = 1; // 1

a += 1;         // 2
printf("%d\n", a);

a = a * 2;      // 4
printf("%d\n", a);

a /= 2;         // 2
printf("%d\n", a);

int b = a % 3;  // 2
printf("%d\n", b);

return 0;

} ```

Однако, не всё так просто, как кажется!

```C

include <stdio.h>

int main(void) { int a = 1; // 1 int b = 2; // 2 int c = a / b; // ? printf("%d\n", c);

float d = a / 2.0;      // !
printf("%f\n", d);

d = a / 2;              // !
printf("%f\n", d);

return 0;

} ```

Работаем с целыми числами – значит и получаем целые числа. Как только добавляем float – уже и результат будет float.

Похоже работают приоритеты операций, скобки:

C int a = 2 + 2 * 2; // 6 int b = (2 + 2) * 2; // 8 int c = 16 / 4 / 4 // 1

ДЗ

Программа для проверки прямоугольности треугольника: - На вход даются 3 числа - длины 3-х сторон треугольника. - Вывести "True", если треугольник прямоугольный, иначе "False".

Тесты: - 3, 4, 5 - 20, 4, 5 - 1.2345678, 2.3456789, 6.7657366252095725