Пишем первые программы на C. CS50 на русском. Лекция 1.2
2026-02-21 01:07 Diff

#статьи

  • 11 янв 2024
  • 0

Дэвид Малан показывает, как писать на C программы с циклами, условиями и пользовательскими функциями, а также рассказывает про ошибки переполнения.

Фото: LordHenriVoton / Getty Images

Программист, консультант, специалист по документированию. Легко и доступно рассказывает о сложных вещах в программировании и дизайне.

CS50 (Computer Science 50) — легендарный курс по информатике от Гарвардского университета. Когда заходит разговор о вкатывании в программирование, опытные разработчики чаще всего советуют именно его в качестве источника базовых знаний. В нём последовательно разбираются логика работы компьютера, простые алгоритмы и основы программирования в визуальной среде Scratch, массивы, устройство и работа памяти, структуры данных, основы языка C (об этом сегодня), Python, SQL, HTML, CSS, JavaScript, Flask и много другое.

Зачем смотреть/читать CS50: по окончании курса вы будете знать, как работает компьютер на уровне процессора и ОЗУ, освоите универсальные принципы программирования (то есть без привязки к конкретному языку), научитесь понимать и читать код, написанный на разных языках.

У нас уже вышло несколько статей на основе уроков CS50:

Пишем первые программы на C. Лекция 1.2 вы находитесь здесь.

CS50 — это самый популярный курс в Гарвардском университете и самый посещаемый массовый открытый онлайн-курс на edX. Все материалы курса доступны бесплатно (в том числе и практические задания), но, если заплатить, можно получить сертификат и дополнительные плюшки.

Мы перевели видеолекции в текстовый формат, снабдили их иллюстрациями, кое-где дополнили объяснения и выкладываем в открытый доступ. Оригинальный курс доступен по лицензии Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) — его можно дорабатывать и распространять бесплатно, но только под исходной лицензией. Так что этот цикл материалов вы также сможете использовать в своей работе или общественной деятельности совершенно свободно и бесплатно в рамках той же лицензии.

Каждая статья из цикла CS50 состоит из следующих материалов:

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

Содержание этого занятия

На прошлом уроке мы познакомились с синтаксисом и основными концепциями языка C. Сейчас приступим к практике и посмотрим, как на нём писать программы. Откроем VS Code и с помощью командной строки создадим файл calculator.c:

code calculator.c

Он автоматически откроется в среде разработки. Начнём с подключения библиотек cs50.h и stdio.h:

#include <cs50.h> #include <stdio.h> int main(void) { }

Теперь создадим простой калькулятор на основе тех операторов, которые мы изучали на прошлом занятии.

Создадим переменные x и y и выведем их сумму x + y:

#include <cs50.h> #include <stdio.h> int main(void) { ​​ int x = get_int("x: "); int y = get_int("y: "); printf("%i\n", x + y); }

Функция printf() в качестве первого аргумента принимает форматную строку "%i\n", определяющую формат вывода, а в качестве второго — выражение, которое выведется на экран.

Мы получили простой калькулятор, который умеет складывать два числа. Скомпилируем его — никаких сообщений об ошибках не поступает. Запустим калькулятор и сложим 1 + 1.

Кадр: CS50 / YouTube

Как видим, всё работает.

Немного изменим код калькулятора — добавим переменную z:

#include <cs50.h> #include <stdio.h> int main(void) { ​​ int x = get_int("x: "); int y = get_int("y: "); int z = x + y; printf("%i\n", z); }

Если мы запустим калькулятор и сложим 1 + 1, то получим тот же результат. Он будет работать и для других значений x и y. Код с переменной z подходит в тех случаях, когда мы планируем дополнять его и будем использовать результат сложения повторно.

Теперь поговорим о стиле нашего кода. Имена x и y в нашем примере отлично подходят для переменных, потому что часто используются в математике. Однако если хочется добавить ясности, то можно изменить названия на first_number и second_number:

#include <cs50.h> #include <stdio.h> int main(void) { ​​ int first_number = get_int("x: "); int second_number = get_int("y: "); printf("%i\n", first_number + second_number); }

Теперь добавим в программу комментарии. Они помогут нам вспомнить, что делает код, когда мы вернёмся к нему и что-нибудь забудем. Строки комментариев начинаются с двух косых черт:

#include <cs50.h> #include <stdio.h> int main(void) { // Запрашиваем у пользователя x ​​ int first_number = get_int("x: "); // Запрашиваем у пользователя y int second_number = get_int("y: "); // Выполняем сложение printf("%i\n", first_number + second_number); }

Комментарии не влияют на работу программы — это просто заметки для вас и других разработчиков. В такой небольшой программе от них мало пользы. Но, по мере того как количество кода будет увеличиваться, заметки не дадут нам забыть, что он делает.

Совет: большинство операционных систем (как минимум ОС семейства Linux, Windows и macOS), в которых ведётся разработка, поддерживают автодополнение кода. Вы можете нажать стрелку вверх, чтобы увидеть всю историю команд и выбрать нужную, вместо того чтобы набирать несколько раз одно и то же. Это заметно ускоряет работу.

Пришло время поработать с большими числами. Например, пусть x и y равны 1 000 000 000. Введём эти числа в командную строку и посмотрим результат:

Кадр: CS50 / YouTube

Как видим, всё получилось.

А теперь пусть x и y будут равны 2 000 000 000:

Кадр: CS50 / YouTube

А здесь, кажется, что-то пошло не так… Произошло переполнение!

Теперь мы понимаем, что тестирование со сложением двух единиц было ненадёжным решением. В последнем примере в компьютере закончилось место для хранения битов. Это случается с типами данных string, int, float, char — все они используют конечное число битов для представления чисел и символов.

Для хранения целого числа используется 32 бита. С их помощью можно представить число 2³², что примерно равно 4 000 000 000. Результат должен умещаться в 32-битном целом числе. Кажется, что нам этого хватит:

2 000 000 000 + 2 000 000 000 = 4 000 000 000

Но дело в том, что компьютеры, кроме положительных, поддерживают отрицательные числа, то есть хранят числа в диапазоне от −2 000 000 000 до 2 000 000 000. Поэтому мы получаем странный вывод в сложении.

Чтобы решить проблему переполнения, будем использовать тип данных long. Поменяем в программе тип переменных x и y с int на long.

#include <cs50.h> #include <stdio.h> int main(void) { // Запрашиваем у пользователя x ​​ int first_number = get_long("x: "); // Запрашиваем у пользователя y int second_number = get_long("y: "); // Выполняем сложение printf("%i\n", first_number + second_number); }

Запустим калькулятор:

Кадр: CS50 / YouTube

Теперь всё получилось!

Но, если мы будем использовать тип long, числа в нём всё равно будут конечными. И это может стать проблемой, если мы хотим сделать калькулятор работоспособным для любых входных данных. Мы ещё вернёмся к этому вопросу позже.

А сейчас рассмотрим условные выражения. В C они записываются так:

if (x < y) { printf("x is less than y\n") }

Добавим блок else, чтобы описать действия при несоблюдении условия:

if (x < y) { printf("x is less than y\n") } else { printf("x is not less than y\n") }

Одна из особенностей синтаксиса языка C — круглые скобки используются как для записи функции, так и для записи логических выражений. А ещё, в нём нет необходимости использовать фигурные скобки, если в них заключена всего одна строка с отступом. Но я рекомендую использовать их всегда — так код будет понятнее.

Если мы хотим написать, что будет при условии x == y, то код будет выглядеть так:

if (x < y) { printf("x is less than y\n") } else { printf("x is not less than y\n") } else if (x == y) { printf("x is equal to y\n") }

Последнее условие мы можем убрать, ведь если не выполняются первые два, то x может быть равен только y.

Оптимизируем код:

if (x < y) { printf("x is less than y\n") } else if (x > y) { printf("x is not less than y\n") } else { printf("x is equal to y\n") }

Убрав третье условие, мы уменьшим время работы программы, так как ей не придётся проверять условие. Не забывайте о таких вещах, когда будете писать код: он должен быть не только правильным, но и быстрым.

Перейдём к решению реальной проблемы: спросим пользователя, сколько баллов он потерял при решении первого набора задач CS50. Я сам потерял там пару баллов в 1996 году. Сравним потери пользователя с моими:

#include <cs50.h> #include <stdio.h> int main(void) { // Запрашиваем у пользователя баллы ​​ int points = get_int("Сколько баллов вы потеряли?"); if (points < 2) { printf("Вы потеряли меньше баллов, чем я.\n"); } else if (points > 2) { printf("Вы потеряли больше баллов, чем я.\n"); } else { printf("Вы потеряли столько же баллов, сколько я.\n"); } }

Теперь мы знаем, как использовать условные выражения, однако наш код всё ещё избыточен. Я жёстко запрограммировал число 2 — потерянное мной количество очков. Если мне нужно будет заменить 2 на 3, то я легко это сделаю. Но предположим, что количество баллов сравнивается не в одном, а в двух, трёх или пяти местах кода. Тогда при замене числа мы наверняка где-нибудь ошибёмся.

Вместо того чтобы переписывать число в разных местах программы, в самом начале создадим переменную, которая будет хранить это значение. А чтобы случайно не присвоить что-то по ошибке, сделаем её константой. Константа говорит компилятору, что ниже в коде её значение изменить нельзя:

#include <cs50.h> #include <stdio.h> int main(void) { const int MINE = 2; // Запрашиваем у пользователя баллы ​​ int points = get_int("Сколько баллов вы потеряли?"); if (points < MINE) { printf("Вы потеряли меньше баллов, чем я.\n"); } else if (points > MINE) { printf("Вы потеряли больше баллов, чем я.\n"); } else { printf("Вы потеряли столько же баллов, сколько я.\n"); } }

В C и других языках существует правило: имя константы всегда пишется заглавными буквами. Это упрощает чтение кода.

А теперь напишем программу, которая будет проверять, чётное или нечётное число ввёл пользователь. Используем для этого синтаксис и приёмы, которые мы уже изучили.

Из математики мы знаем, что число считается чётным, если его деление на два даёт остаток 0, а нечётным — если остаток равен 1. Воспользуемся оператором %, который при делении числителя на знаменатель возвращает не частное, а остаток от деления.

#include <cs50.h> #include <stdio.h> int main(void) { int n = get_int("n: "); // Если n чётное if (n % 2 == 0) { printf("чётное\n"); } // Если n нечётное else { printf("нечётное\n"); } }

Обратите внимание на ==. В языке C это знак равенства, а = — знак присваивания. Возможно, вам это решение покажется странным, но оно было принято давно и нам приходится с ним жить. В некоторых языках, таких как JavaScript, используется ===.

Теперь давайте напишем программу, которая спрашивает у пользователя, согласен ли он с каким-нибудь выражением. В качестве ответа будет приниматься один символ, например y или n. Другие символы игнорируются.

Программа выглядит так:

#include <cs50.h> #include <stdio.h> int main(void) { // Запрашиваем согласие пользователя char c = get_char("Вы согласны?"); // Проверяем, согласен ли он if (c == 'y') { printf("Согласен\n"); } if (c == 'n') { printf("Не согласен\n"); } }

А что делать, если пользователь решит ответить Y или N? Учтём это в коде и добавим в наши условия логическое ИЛИ. В языке C это две вертикальные полосы ||.

#include <cs50.h> #include <stdio.h> int main(void) { // Запрашиваем согласие пользователя char c = get_char("Вы согласны?"); // Проверяем, согласен ли он if (c == 'y' || c == 'Y') { printf("Согласен\n"); } if (c == 'n' || c =='N') { printf("Не согласен\n"); } }

Есть альтернатива этому решению: можно преобразовывать вводимые символы в строчные буквы, чтобы не использовать ||. Но об этом мы поговорим на других лекциях.

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

Теперь посмотрим, как в языке C организовать циклы. Вспомним позапрошлый урок, где мы учили кошку мяукать, и напишем программу:

#include <stdio.h> int main(void) { printf("мяу\n"); printf("мяу\n"); printf("мяу\n"); }

Сейчас она написана не совсем корректно — в ней есть повторения. Чтобы их убрать, используем циклы. Вот один из самых простых:

while (true) { printf("мяу\n"); }

В скобках стоит логическое выражение — условие выполнения цикла. Он будет работать до тех пор, пока условие истинно, а когда станет ложным — прервётся. Если бы мы хотели, чтобы цикл выполнялся вечно, то могли бы поставить заведомо истинное условие (1 == 1), (2 > 1) и так далее. В языке C в таких случаях используют логические значения true и false.

Чтобы организовать корректную работу цикла, добавим в него счётчик. В C и многих других языках существует соглашение: для счётчика используют переменную i c первоначальным значением 0.

int i = 0; while (i < 3) { printf("мяу\n"); i = i + 1; }

У нас есть возможность усовершенствовать эту программу. Применим то, что называется синтаксическим сахаром:

int i = 0; while (i < 3) { printf("мяу\n"); i = i++; }

Выражение i = i + 1 мы поменяли на i = i++. Такие изменения не влияют на работу программы, но делают код короче.

Разберём алгоритм программы:

  • Мы начинаем с инициализации переменной i.
  • Затем компьютер проверяет условие i < 3. Если оно верно, то выполняется всё, что заключено в фигурные скобки, а именно — программа печатает мяу.
  • Значение i увеличивается на единицу.
  • Теперь компьютер перепроверяет условие, чтобы убедиться, что i не стала больше 3. Если это не так, он выполняет всё, что находится в блоке.
  • После трёх повторений условие станет ложным и выполнение цикла заканчивается.
  • Компьютер переходит к командам, следующим за циклом.

Конечно, вы можете начать цикл не с 0, а, например, с 1. Тогда, чтобы тело цикла выполнилось три раза, придётся изменить условие:

int i = 0; while (i <= 3) { printf("мяу\n"); i = i++; }

Обратите внимание, как записывается знак «меньше или равно»: сначала знак «меньше», а потом знак равенства без пробелов между ними: <=.

Мы могли бы установить i равным 2, 10 или другому числу и соответствующим образом изменить условие. Но лучше придерживаться основ — начинать отсчёт с нуля и увеличивать счётчик до нужного значения.

Возможно, вы захотите вести обратный отсчёт. Установите i = 3 и уменьшайте счётчик до тех пор, пока он не станет равен 0, например:

int i = 3; while (i > 0) { printf("мяу\n"); i = i−−; }

Эту задачу можно решить с помощью цикла for.

for часто используется в C и других языках программирования. С ним наш мяукающий код будет выглядеть так:

for (int i = 0; i < 3; i++) { printf("мяу\n"); }

Рассмотрим выражение в круглых скобках:

  • Сначала мы инициализируем счётчик: i = 0.
  • Затем инициализируется условие, которое будет проверяться каждый раз при прохождении цикла. Мы проверяем, i меньше 3 или нет.
  • И последнее — это увеличение счётчика на 1.

В сущности, оба способа одинаковы — циклы for и while используются для одного и того же действия. Но между ними есть различия. Обратите внимание на переменную i в цикле for — она находится в круглых скобках. Это означает, что i будет существовать только в этих четырёх строках кода. В цикле while переменная i находится вне скобок, то есть существует за пределами условий цикла.

Теперь создадим нашу собственную функцию на языке C. Дадим ей название meow() («мяу»). Теперь программа будет выглядеть так:

#include <stdio.h> void meow(void) int main(void) { for (int i = 0; i < 3; i++) { meow(); } } void meow(void) { printf("мяу\n"); }

Команда void meow(void) означает, что у функции нет входных данных и она ничего не возвращает. Её цель — выводить числа и строки на экран.

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

Мы поставили в теле цикла for вызов функции meow(). Теперь немного исправим её — пусть она мяукает несколько раз. Для этого перенесём в неё цикл for, а количество мяуканий будем передавать в качестве аргумента n. Это будет целое число, поэтому присвоим переменной n тип int.

#include <stdio.h> void meow(int n) int main(void) { meow(3); } // Настраиваем многократное мяукание void meow(int n) { for (int i = 0; i < n; i++) { printf("мяу\n"); } }

В объявление функции meow() вместо пустого значения мы добавили аргумент n.

Теперь давайте создадим функцию, которая бы не только принимала аргументы, но и возвращала какое-нибудь значение. На языке C мы легко можем это сделать.

Напишем программу, рассчитывающую цену товара со скидкой. Пусть она выглядит так:

#include <cs50.h> #include <stdio.h> int main(void) { float regular = get_float("Обычная цена: "); float sale = regular * .85; printf("Цена со скидкой: %.2f\n", sale); }

Здесь regular — первоначальная цена, а sale — цена со скидкой.

Создадим пользовательскую функцию, которая в качестве аргумента принимала бы первоначальную цену, а возвращала бы значение цены со скидкой:

#include <cs50.h> #include <stdio.h> float discount(float price) int main(void) { float regular = get_float("Обычная цена: "); float sale = discount(regular); printf("Цена со скидкой: %.2f\n", sale); } float discount(float price) { return price * .85; }

Все переменные мы сделали числами с плавающей точкой.

Обратите внимание, что функция discount() не печатает рассчитанное знач��ние, а возвращает его в вызывающую функцию. Для этого используется ключевое слово return.

Функции могут принимать не один аргумент, а два, три и более. Введём в качестве аргумента функции discount() величину скидки. Это позволит задать любую скидку — не только 15%.

#include <cs50.h> #include <stdio.h> float discount(float price, int percentage) int main(void) { float regular = get_float("Обычная цена: "); int percent_off = get_ing("Размер скидки: "); float sale = discount(regular, percent_off); printf("Цена со скидкой: %.2f\n", sale); } float discount(float price, int percentage) { return price * (100 − percentage) / 100; }

Всё получилось. Наша функция вычисляет стоимость товара со скидкой, значение которой мы можем ввести сами.

А теперь используем полученные нами знания для разработки небольшой игры. Вспомните Super Mario Bros.: там в небе за вопросительными знаками были спрятаны монеты.

Дэвид на фоне своих слайдов во время чтения курса. Здесь он объясняет правила игры Mario
Кадр: CS50 / YouTube

Мы пока не сможем создать красочный мир игры. Давайте просто выведем несколько вопросительных знаков:

#include <cs50.h> #include <stdio.h> int main(void) { printf("????\n"); }

Усовершенствуем программу — используем циклы for:

#include <cs50.h> #include <stdio.h> int main(void) { for (int i = 0; i < 4; i++) { printf("?"); } printf("\n"); }

Как видите, после цикла for мы вывели на печать знак перевода строки \n. В цикле мы его поставить не можем, так как каждый вопросительный знак будет печататься на новой строке.

Усложним нашу программу — пусть она спрашивает у пользователя, сколько вопросительных знаков нужно вывести на экран. Для этого познакомимся с ещё одним видом циклов — do while. Он похож на цикл while, но проверяет условие не перед, а после своего тела.

Цикл do while полезен, когда вы хотите сделать что-то независимое от условия, а само условие проверить в конце:

#include <cs50.h> #include <stdio.h> int main(void) { for (int i = 0; i < n/; i++) { printf("?"); } printf("\n"); }

Здесь пользователь вряд ли введёт n = 0 или n = −100, так как это не имеет смысла. А когда n будет больше или равно 1, программа выйдет из цикла и управление перейдёт к следующей по порядку команде.

Теперь рассмотрим ту часть игры, где Марио спускается в подземелье и перед ним появляется стена из кирпичей.

Дэвид на фоне своих слайдов во время чтения курса. Мы видим стену из кирпичей — препятствие для Марио
Кадр: CS50 / YouTube

Выведем на печать что-то похожее на квадрат, как на изображении. Так как у нас на нём кирпичи, используем для вывода символ #.

Чтобы кирпичи были расположены в несколько строк, будем использовать цикл в цикле. Программа выглядит так:

#include <cs50.h> #include <stdio.h> int main(void) { int n; do { n = get_int("Ширина: "); } while (n <1 ); // Для каждой строки for (int i = 0; i < n; i++) { // Для каждого столбца for (int j = 0; i < j; i++) { // Печатаем кирпичек printf("#"); } } // Переходим к следующей строке printf("\n"); }

Первый цикл используется для подсчёта строк сверху вниз, а второй цикл в каждой строке посимвольно выводит знаки на экран, наподобие старой пишущей машинки.

Запустим программу. Вот что у нас получилось:

Кадр: CS50 / YouTube

Квадрат получился не идеальным, так как символ # в длину меньше, чем в ширину, но это уже особенности шрифта. Будем считать, что задача решена.

А теперь вернёмся к нашему калькулятору. Снова рассмотрим проблему переполнения при сложении больших чисел, но на этот раз будем работать с числами с плавающей точкой. Изменим тип переменных с int на float и вместо сложения проведём деление.

Добавим в код ещё одну переменную float z = x / y и выведем её на экран:

#include <cs50.h> #include <stdio.h> int main(void) { // Запрашиваем у пользователя x ​​ int first_number = get_float("x: "); // Запрашиваем у пользователя y int second_number = get_float("y: "); // Выполняем деление float z = x / y; printf("%i\n", z; }

Запустим калькулятор и зададим x = 2, y = 3:

Кадр: CS50 / YouTube

Мы получили ответ — число с шестью знаками после точки. С такой точностью компьютер возвращается результат по умолчанию.

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

#include <cs50.h> #include <stdio.h> int main(void) { // Запрашиваем у пользователя x ​​ int first_number = get_float("x: "); // Запрашиваем у пользователя y int second_number = get_float("y: "); // Выполняем деление float z = x / y; printf("%.2f\n", z; }

Запустим программу:

Кадр: CS50 / YouTube

А теперь попробуем вывести 50 знаков после точки:

#include <cs50.h> #include <stdio.h> int main(void) { // Запрашиваем у пользователя x ​​ int first_number = get_float("x: "); // Запрашиваем у пользователя y int second_number = get_float("y: "); // Выполняем деление float z = x / y; printf("%.50f\n", z; }

Запустим калькулятор:

Кадр: CS50 / YouTube

Оказывается, мы не только не можем сложить миллиарды, но даже получить точный ответ при делении двух чисел.

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

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

В процессе работы программы компьютер может менять тип данных, которые он обрабатывает. Сделаем x и y целыми числами, а z оставим числом с плавающей точкой:

#include <cs50.h> #include <stdio.h> int main(void) { // Запрашиваем у пользователя x ​​ int first_number = get_int("x: "); // Запрашиваем у пользователя y int second_number = get_int("y: "); // Выполняем деление float z = x / y; printf("%.50f\n", z; }

Запустим программу. Пусть x = 2, а y = 3:

Кадр: CS50 / YouTube

Результат неожиданный. На самом деле должно получиться бесконечное число 0,6666666666 и так далее. Дело в том, что в языке С при делении целого числа на целое число получается целое число, поэтому компьютер просто отбрасывает ту часть числа, которая меньше 0. Эта функция в языке C называется усечением. Результат был меньше 1, и компьютер выдал 0.

А теперь рассмотрим ещё один пример. Пусть x = 4, а y = 3. При делении 4 на 3 должна получиться бесконечная десятичная дробь 1,333333 и так далее. Запустим калькулятор:

Кадр: CS50 / YouTube

Компьютер разделил 4 на 3 как целые числа, и в результате выдал 1.0000. Здесь также произошло усечение дробной части числа.

Чтобы исправить ситуацию, используем то, что в C и других языках называется преобразованием типов:

#include <cs50.h> #include <stdio.h> int main(void) { // Запрашиваем у пользователя x ​​ int first_number = get_int("x: "); // Запрашиваем у пользователя y int second_number = get_int("y: "); // Выполняем деление float z = (float)x / (float)y; printf("%.50f\n", z; }

Здесь мы сообщаем компьютеру, что хотим обрабатывать переменные int как числа с плавающей точкой. Запустим наш калькулятор и опять разделим 2 на 3:

Кадр: CS50 / YouTube

Результат уже ближе к правильному, хотя погрешность всё равно остаётся.

Вспомним первую лекцию. У нас было три бита, и мы помещали в них числа от 0 до 7, то есть от 000 до 111. Я тогда задал вопрос: а как мы будем считать до 8? Кто-то сказал, что нужен четвёртый бит. Действительно, в этом случае число 8 можно было бы представить как 1000.

Но допустим, что у вас нет места для четвёртого бита. Тогда, прибавив 1 к числу 111, вы опять получите 000. Происходит то, что называется переполнением, — число, равное 8, не помещается в три бита, и они заполняются нулями.

Какой бы странной ни казалась эта проблема, люди с нею уже сталкивались. Возможно, вы помните или читали о проблеме Y2K. 1 января 2000 года компьютеры должны были обновить часы. Но многие системы, особенно написанные давно, в датах хранили только две последние цифры года, чтобы сэкономить место в памяти. Для 2000 года это было просто 00. И если программа добавляла к такой дате префикс 19, то оказывалось, что из 1999 года мы вернулись в 1900-й.

К счастью, к тому времени удалось поправить много кода, и эту проблему в основном решили. Однако в следующий раз она может возникнуть 19 января 2038 года. В некоторых программах используется время, представляющее собой количество секунд, прошедшее с полуночи 1 января 1970 года. Но в старых системах используется хранение секунд в виде 32-битного целого со знаком. Самая поздняя точка во времени, которая может быть представлена таким форматом, — это 03:14:07 19 января 2038 года.

Время позже этой даты заставит поле данных стать отрицательным, а отрицательное число может быть воспринято программами как время в 1901 году. В результате любые расчёты, использующие дату позже 19 января 2038 года, могут привести к ошибочным вычислениям.

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

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

#include <cs50.h> #include <stdio.h> int main(void) { float amount = get_float("Количество долларов: "); int pennies = amount * 100; printf("Пенни: %i\n", pennies); }

Запустим программу и введём несколько значений переменной amount:

Кадр: CS50 / YouTube

Как видите, при вводе amount равном 4.2 программа выдаёт неправильный результат. Конечно, ничего страшного, если кассир в супермаркете обсчитает вас на одно пенни, но представьте себе последствия подобной ошибки в финансовых операциях или научных измерениях.

Попробуем исправить эту ошибку. Представим, что компьютер хранит 4 доллара и 19,99999 центов или около того. Избавимся от неточности путём округления результата. Подключим библиотеку математических функций math.h и используем функцию round(), которая в ней содержится:

#include <cs50.h> #include <math.h> #include <stdio.h> int main(void) { float amount = get_float("Количество долларов: "); int pennies = round(amount * 100); printf("Пенни: %i\n", pennies); }

Снова запустим программу:

Кадр: CS50 / YouTube

Теперь всё правильно.

О таких вещах забывать нельзя. К сожалению, даже профессиональные программисты не всегда уделяют должное внимание подобным мелочам. И цель наших занятий не просто научить вас программировать, а ещё и научить понимать то, что происходит, так сказать, «под капотом» программного кода.

Иногда обществу приходится дорого платить за такие ошибки. Например, несколько лет назад стало известно о баге в системе управления самолёта Boeing. Система должна была перегружаться каждые 248 дней, иначе рейс прямо в ходе полёта мог перейти в отказоустойчивый режим и обесточить свои генераторы, что привело бы к катастрофе.

Это произошло потому, что программное обеспечение использовало 32-битное число, отсчитывающее десятые доли секунды для отслеживания параметров электрической мощности генераторов. Через 248 дней происходило переполнение, и в качестве побочного эффекта система отключала бы электроснабжение. Решение компании Boeing было связано с использованием 32-битной операционной системы. В настоящее время она выпустила патч с исправлением.

Чем больше аппаратного обеспечения мы носим с собой и чем больше используем подобных устройств, тем с большим количеством проблем нам придётся столкнуться.

Подведём итоги сегодняшней лекции:

  • Если вы планируете использовать результат какого-либо вычисления в коде повторно, то сохраните его в отдельную переменную.
  • Вы можете нажать стрелку вверх в редакторе кода, чтобы увидеть всю историю команд и выбрать нужную, вместо того чтобы набирать несколько раз одно и то же. Это ускорит работу.
  • Помните о проблеме переполнения при работе с типами данных int и float — все они используют конечное число битов для представления. Используйте тип данных long, чтобы этой проблемы избежать.
  • В C объявление функции всегда ставится в начале программы. В некоторых других языках программирования, например в Python, функции можно указывать в любом месте кода.
  • Для правильного округления чисел используйте функцию round() из библиотеки математических функций math.h.
Бесплатный курс по Python ➞
Мини-курс для новичков и для опытных кодеров. 4 крутых проекта в портфолио, живое общение со спикером. Кликните и узнайте, чему можно научиться на курсе. Смотреть программу