Код
#статьи

Пишем первые программы на C. CS50 на русском. Лекция 1.2

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

Фото: LordHenriVoton / Getty Images

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

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

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

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

Почему мы перевели CS50 и как устроена каждая статья по курсу

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

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

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

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

Лекции ведёт

Дэвид Дж. Малан

Американский учёный, профессор Гарвардского университета.

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

Пишем код на C и создаём калькулятор

На прошлом уроке мы познакомились с синтаксисом и основными концепциями языка 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. В отличие от строк, в переменных такого типа используются одинарные кавычки, а не двойные.

Циклы while

Теперь посмотрим, как в языке 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

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;
}

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

Создаём игру на C

А теперь используем полученные нами знания для разработки небольшой игры. Вспомните 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.

Больше интересного про код — в нашем телеграм-канале. Подписывайтесь!

Проверьте свой английский. Бесплатно ➞
Нескучные задания: small talk, поиск выдуманных слов — и не только. Подробный фидбэк от преподавателя + персональный план по повышению уровня.
Пройти тест
Понравилась статья?
Да

Пользуясь нашим сайтом, вы соглашаетесь с тем, что мы используем cookies 🍪

Ссылка скопирована