Раздел 4 — Выражения, ввод и вывод

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

Прогресс курса Раздел 4 из 28

Что вы освоите в этом разделе

2 академических часа теории и 2 часа практики: практическая работа №2 «Линейные программы» — на портале. Все программы страницы — полные; собирайте их командой gcc -std=c11 -Wall -Wextra -pedantic файл.c -o файл (в Linux с <math.h> — ещё -lm в конце).
01

Структура программы

Программа раздела — линейная: инструкции выполняются по одной сверху вниз, каждая ровно один раз. Это базовая структура «следование» из раздела 2. Пример — алгоритм «сколько автобусов» из раздела 2:

/* Сколько автобусов заказать: N учеников, c мест в автобусе */
#include <stdio.h>                         /* 1. подключение библиотек */

int main(void)                             /* 2. главная функция */
{
    int students = 0, seats = 0;             /* 3. объявления */
    printf("Students and seats: ");         /* 4. ввод */
    scanf("%d %d", &students, &seats);

    int buses = (students + seats - 1) / seats; /* 5. вычисления */
    int free_seats = buses * seats - students;

    printf("Buses: %d, free seats: %d\n", buses, free_seats);  /* 6. вывод */
    return 0;                                 /* 7. код завершения */
}
Students and seats: 50 45          Students and seats: 90 45
Buses: 2, free seats: 40           Buses: 2, free seats: 0

Почему (students + seats - 1) / seats округляет вверх. Целое деление отбрасывает остаток. Прибавка seats - 1 — на единицу меньше, чем нужно для «лишнего» автобуса: если students делится на seats нацело, прибавка не дотягивает до следующего целого (90 + 44 = 134, 134 / 45 = 2); если остаток есть хотя бы 1 — дотягивает (91 + 44 = 135, 135 / 45 = 3). Формула верна для целых положительных чисел.

Комментарии пишутся между /* и */ или от // до конца строки (с C99). Компилятор их не читает: комментарий объясняет зачем, а не пересказывает код.

02

Присваивание, составные операции, инкремент

x = выражение — вычислить правую часть и записать в x. В C присваивание — операция, у неё есть значение (присвоенное), поэтому a = b = 0 работает: сначала b = 0, затем a = 0.

ЗаписьТо же самое
sum += x;sum = sum + x;
n -= 2;n = n - 2;
price *= 1.2;price = price * 1.2;
rest /= 10;rest = rest / 10;
rest %= 10;rest = rest % 10;
count++; или ++count;count = count + 1;
count--; или --count;count = count - 1;

У ++ две формы. Отдельной инструкцией они не отличаются. Внутри выражения префиксная ++i даёт новое значение, постфиксная i++ — старое:

int i = 5;
int a = ++i;     /* i = 6, a = 6 */
int b = i++;     /* b = 6, i = 7 */
Неопределённое поведение. Изменять переменную дважды в одном выражении или изменять и читать её без определённого порядка нельзя: i = i++;, a = i++ + i++;, printf("%d %d", i, i++); — результат не определён стандартом и разный у разных компиляторов. gcc с -Wall предупреждает: operation on 'i' may be undefined [-Wsequence-point]. Правило курса: ++ и -- — только отдельной инструкцией или в заголовке цикла.
03

Арифметика: деление и остаток

Операции + - * / работают как в математике, с одним отличием: если оба операнда целые, деление целое — дробная часть отбрасывается (раздел 3, пункт 06). Операция % — остаток от деления, только для целых.

ВыражениеЗначениеПояснение
17 / 53целое деление
17 % 5217 = 5 · 3 + 2
-17 / 5−3отбрасывание дробной части — к нулю, не вниз
-17 % 5−2знак остатка — как у делимого: −17 = 5 · (−3) + (−2)
17 / 5.03.4один операнд double — деление вещественное
1 / 2 * 4.00.0слева направо: 1 / 2 = 0, затем 0 · 4.0
4.0 * 1 / 22.0сначала 4.0 · 1 = 4.0, затем 4.0 / 2
7 / 0—деление целого на 0 — неопределённое поведение, обычно аварийное завершение

Пара / и % разбирает число на части. Секунды → часы, минуты, секунды:

/* Секунды от начала суток → часы, минуты, секунды */
#include <stdio.h>

int main(void)
{
    int total = 0;
    printf("Seconds: ");
    scanf("%d", &total);

    int hours = total / 3600;
    int minutes = total % 3600 / 60;
    int seconds = total % 60;
    printf("%02d:%02d:%02d\n", hours, minutes, seconds);
    return 0;
}
Seconds: 3725      ->  01:02:05
Seconds: 86399     ->  23:59:59
Seconds: 0         ->  00:00:00

total % 3600 / 60: % и / одного приоритета и выполняются слева направо — сначала остаток после целых часов (3725 % 3600 = 125), потом минуты в нём (125 / 60 = 2). Последняя цифра числа — n % 10, число без неё — n / 10; это понадобится для цифр числа в разделе 6.

04

Приоритеты операций

Операции выполняются по приоритету, при равном — по ассоциативности. Таблица — для операций, которые встретятся до раздела 10; выше в таблице — раньше выполняется.

ПриоритетОперацииПорядок при равном приоритете
1() вызов функции, a++ a--слева направо
2++a --a -a ! (тип) sizeofсправа налево
3* / %слева направо
4+ -слева направо
5< <= > >=слева направо
6== !=слева направо
7&&слева направо
8||слева направо
9?:справа налево
10= += -= и другие присваиваниясправа налево

Примеры: 2 + 3 * 4 = 14; (2 + 3) * 4 = 20; 10 - 4 - 3 = 3 (слева направо: (10 - 4) - 3); -2 * 3 = −6; (double)7 / 2 = 3.5 — приведение выполняется раньше деления. Математическая формула (a + b) / (2c) в C — (a + b) / (2 * c): без скобок (a + b) / 2 * c — это (a + b) / 2 · c.

Таблицу не нужно учить наизусть. Нужно помнить три вещи: умножение и деление раньше сложения; сравнение раньше && и ||; присваивание — последним. Во всех остальных случаях ставьте скобки — их читают люди, и лишняя пара скобок дешевле ошибки.
05

Математические функции

Функции из <math.h> принимают и возвращают double:

ФункцияЧто считаетПример
sqrt(x)квадратный корень, x ≥ 0sqrt(2.0) = 1.41421…
pow(x, y)xypow(1.1, 3) = 1.331
fabs(x)модуль вещественного; для целого — abs из <stdlib.h>fabs(-2.5) = 2.5
floor(x), ceil(x)округление вниз и вверхfloor(-2.5) = −3, ceil(2.1) = 3
round(x)к ближайшему целому, половина — от нуляround(2.5) = 3, round(-2.5) = −3
sin(x), cos(x)угол в радианахsin(PI / 6) ≈ 0.5
exp(x), log(x)ex и натуральный логарифмlog(exp(2.0)) = 2

Расстояние между точками (x1; y1) и (x2; y2):

/* Расстояние между двумя точками */
#include <stdio.h>
#include <math.h>

int main(void)
{
    double x1 = 0, y1 = 0, x2 = 0, y2 = 0;
    printf("x1 y1 x2 y2: ");
    scanf("%lf %lf %lf %lf", &x1, &y1, &x2, &y2);

    double dx = x2 - x1;
    double dy = y2 - y1;
    double distance = sqrt(dx * dx + dy * dy);
    printf("Distance: %.3f\n", distance);
    return 0;
}
x1 y1 x2 y2: 0 0 3 4        ->  Distance: 5.000
x1 y1 x2 y2: 0 0 1 1        ->  Distance: 1.414

Квадрат — умножением dx * dx, а не pow(dx, 2): короче и точнее. pow — для дробных и больших степеней.

Нет константы π. M_PI не входит в стандарт C и при сборке с -std=c11 не объявлена: error: 'M_PI' undeclared. Объявите свою: const double PI = 3.14159265358979;. И помните про целое деление внутри формул: 9 / 5 * c + 32 — это 1 * c + 32; нужно 9.0 / 5 * c + 32.
06

Вывод: printf

printf(формат, значения…) выводит строку формата, заменяя каждую спецификацию — от % до буквы — очередным значением:

СпецификацияТип значенияПример → вывод
%dint, charprintf("%d", 42) → 42
%uunsigned int
%lldlong long
%fdouble (и float)printf("%f", 2.5) → 2.500000
%.2fdouble, 2 знака после точкиprintf("%.2f", 2.675) → 2.67 (см. ниже)
%edouble в экспоненциальной записиprintf("%e", 12345.0) → 1.234500e+04
%gdouble, короткая запись без лишних нулейprintf("%g", 2.5) → 2.5
%cсимволprintf("%c", 'A') → A
%sстрокаprintf("%s", "abc") → abc
%%знак процентаprintf("10%%") → 10%

Ширина поля — число после %: значение дополняется пробелами слева до этой ширины. Минус — выравнивание влево, ноль — дополнение нулями:

printf("[%5d]\n", 42);        /* [   42] */
printf("[%-5d]\n", 42);       /* [42   ] */
printf("[%05d]\n", 42);       /* [00042] */
printf("[%8.3f]\n", 3.14159); /* [   3.142] — 8 символов всего, 3 после точки */
printf("[%-8s]|\n", "abc");   /* [abc     ]| */

Так строятся таблицы с ровными столбцами. Специальные символы в строке: \n — перевод строки, \t — табуляция, \" — кавычка, \\ — обратная косая черта.

Округление при выводе — двоичное. %.2f округляет хранимое значение, а 2.675 в double хранится как 2.67499999999999982…, поэтому printf("%.2f", 2.675) выводит 2.67, а не 2.68. Для денег, где важно «по правилам», округляют явно: round(x * 100) / 100. А несовпадение спецификации и типа (%d для double, %f для int) — неопределённое поведение; gcc с -Wall предупреждает о нём (-Wformat), и предупреждение нужно исправлять.
07

Ввод: scanf

scanf(формат, адреса…) читает из ввода значения по спецификациям и записывает их в переменные. Ему нужны не значения переменных, а их адреса — знак & перед именем (что такое адрес, подробно — раздел 10):

int n;
double price;
char grade;
scanf("%d", &n);          /* целое */
scanf("%lf", &price);     /* double — %lf, не %f */
scanf(" %c", &grade);     /* пробел перед %c пропускает пробелы и переводы строки */
Типprintfscanf
int%d%d
long long%lld%lld
double%f%lf
char%c %c (с пробелом)

%d и %lf сами пропускают пробелы, табуляции и переводы строки перед числом, поэтому числа можно вводить в одной строке через пробел или каждое на своей. %c читает любой следующий символ, в том числе перевод строки после предыдущего числа, — поэтому перед ним пробел.

scanf возвращает число успешно прочитанных значений. Если вместо числа введено abc, чтение не удастся: scanf("%d", &n) вернёт 0, а n останется без значения. Программа, которая не смотрит на результат scanf, считает дальше по мусору:

/* Что возвращает scanf */
#include <stdio.h>

int main(void)
{
    int a = 0, b = 0;
    printf("a b: ");
    int read = scanf("%d %d", &a, &b);
    printf("read %d, a = %d, b = %d\n", read, a, b);
    return 0;
}
a b: 12 34         ->  read 2, a = 12, b = 34
a b: 12 x          ->  read 1, a = 12, b = 0
a b: x 34          ->  read 0, a = 0, b = 0

Проверка результата — if (scanf(…) != 2) — делается ветвлением, оно в разделе 5. До тех пор программы курса считают, что ввод верный, а начальные значения переменных задаются, чтобы вместо мусора был хотя бы 0.

Частые ошибки: забыт & — scanf("%d", n): gcc предупреждает format '%d' expects argument of type 'int *', but argument 2 has type 'int', а программа аварийно завершается при вводе; %f для double — предупреждение expects argument of type 'float *', but argument 2 has type 'double *' и неверное значение; текст внутри формата scanf("Enter %d", &n) — scanf требует, чтобы пользователь ввёл и слово Enter. Подсказка для пользователя выводится отдельным printf.
08

Линейная программа от модели до тестов

Задача: товар стоит P ₽ без НДС, куплено q штук со скидкой d %. Посчитать сумму без скидки, скидку, НДС 20 % на сумму со скидкой и итог. Все суммы — в рублях с копейками.

  1. Модель. sum = P · q; discount = sum · d / 100; vat = (sum − discount) · 0,20; total = sum − discount + vat. Каждая сумма округляется до копеек: x = round(x · 100) / 100. P, d — double, q — int.
  2. Тесты до программы. 1500, 3, 10 → 4500.00, 450.00, 810.00, 4860.00. 100, 1, 0 → 100.00, 0.00, 20.00, 120.00.
  3. Программа:
/* Покупка: сумма, скидка, НДС, итог */
#include <stdio.h>
#include <math.h>

const double VAT_RATE = 0.20;

int main(void)
{
    double price = 0, discount_pct = 0;
    int quantity = 0;
    printf("Price, quantity, discount %%: ");
    scanf("%lf %d %lf", &price, &quantity, &discount_pct);

    double sum = round(price * quantity * 100) / 100;
    double discount = round(sum * discount_pct) / 100;   /* sum * d / 100, в копейках */
    double vat = round((sum - discount) * VAT_RATE * 100) / 100;
    double total = sum - discount + vat;

    printf("Sum:      %10.2f\n", sum);
    printf("Discount: %10.2f\n", discount);
    printf("VAT:      %10.2f\n", vat);
    printf("Total:    %10.2f\n", total);
    return 0;
}
Price, quantity, discount %: 1500 3 10      Price, quantity, discount %: 100 1 0
Sum:         4500.00                         Sum:          100.00
Discount:     450.00                         Discount:       0.00
VAT:          810.00                         VAT:           20.00
Total:       4860.00                         Total:         120.00
  1. Тестирование: выводы совпали с тестами. Ещё один тест — с копейками: 99.99, 7, 5 → сумма 699.93, скидка 35.00 (34,9965 ₽ округлено), НДС 132.99, итог 797.92.

discount_pct, а не d; VAT_RATE — константа; в формате %%, потому что одиночный % начал бы спецификацию. Ширина %10.2f выравнивает суммы по правому краю — так читают денежные таблицы.

Ключевые выводы раздела

Запомните главное

  • Линейная программа: подключения, объявления, ввод, вычисления, вывод, return 0
  • Деление целых — целое, к нулю; % — остаток со знаком делимого; / и % разбирают число на части
  • Порядок вычисления — по приоритетам и слева направо; где не уверены — скобки
  • ++ и -- — отдельной инструкцией; двойное изменение переменной в одном выражении — UB
  • Функции <math.h> работают с double; π объявляется константой; в Linux — -lm
  • printf: спецификация совпадает с типом; %.2f — знаки после точки, %8.2f — ширина поля
  • scanf принимает адреса (&), для double — %lf, и возвращает число прочитанных значений
Связи раздела. Опирается на раздел 2 — модель задачи и тесты до программы, следование; раздел 3 — типы, переполнение, погрешность, преобразования, константы. Нужен для: раздела 5 — сравнения и логические операции в условиях, проверка результата scanf; раздела 6 — составные присваивания и ++ в циклах, / и % для цифр числа; раздела 10 — что такое & и почему он нужен scanf.
Практическая работа №2 и тест по разделу 4 — на учебном портале. Задания по вариантам, критерии оценивания и сдача — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Раздел 3: Типы данных и память Все разделы Раздел 5: Ветвления