Язык Си в примерах/Учимся складывать

Материал из Викиучебника — открытых книг для открытого мира
Перейти к навигации Перейти к поиску

Язык Си в примерах


  1. Компиляция программ
  2. Простейшая программа «Hello World»
  3. Учимся складывать
  4. Максимум
  5. Таблица умножения
  6. ASCII-коды символов
  7. Верхний регистр
  8. Скобочки
  9. Факториал
  10. Степень числа
  11. Треугольник Паскаля
  12. Корень уравнения
  13. Система счисления
  14. Сортировка
  15. Библиотека complex
  16. Сортировка на основе qsort
  17. RPN-калькулятор
  18. RPN-калькулятор на Bison
  19. Простая грамматика
  20. Задача «Расчёт сопротивления схемы»
  21. Простая реализация конечного автомата
  22. Использование аргументов командной строки
  23. Чтение и печать без использования stdio
  24. Декодирование звукозаписи в формате ADX

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

Дано
два разделенных пробельными символами целых чисел (в «текстовом» десятичном представлении) на стандартном вводе программы.
Найти
значение суммы (разности, произведения, частного от деления — если имеет смысл) этих двух чисел.
Новые элементы языка
переменные; тип double; операторы &, *, ? : контекста выражения и оператор if контекста утверждения; макроподстановка assert, функции printf, scanf.

Вариант «простой»[править]

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

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

#include <assert.h>
#include <stdio.h>

int main ()
{
  int a, b;
  int r = scanf ("%d%d", &a, &b);
  assert (r == 2);
  printf ("%d\n", a + b);
  return 0;
}

Рассмотрим выполнение этой программы почти с ее завершения — вызова функции printf.[1] Данная функция выведет целое число в десятичной форме (согласно указателю преобразования %d), завершая вывод переводом строки (\n).

Число, которое будет выведено, является результатом вычисления выражения a + b — или же, проще говоря, — суммой значений переменных a и b.

Вводимые пользователем числа помещаются в эти переменные функцией scanf, вызываемой в коде выше как scanf ("%d%d", &a, &b).[2] Здесь, как и в printf, используется указатель преобразования %d, означающий на этот раз считывание числа в десятичной форме (возможно — предваряемого пробелами). Поскольку указатель повторен дважды, два числа будут считаны и помещены в упомянутые в аргументах две переменные a и b. (Необходимый здесь унарный оператор & оставим пока без внимания.)

Как и printf, функция scanf объявлена в заголовке (англ. header) stdio.h.[3]

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

Для проверки соответствия ввода пользователя требованиям программы мы сохраняем (=) результат выполнения scanf — количество успешно измененных переменных — в целочисленной переменной с именем r (int r), после чего требуем равенства ее значения двум (assert (r == 2);.)

Действие макроподстановки assert заключается в вычислении выражения, переданного ей первым (и единственным) аргументом и аварийном завершении программы в случае, если полученное значение — ноль («логическая ложь».)[4]

Наконец, в самом начале функции main определены (помимо упомянутой уже r) целочисленные переменные a и b. Их значение в начале выполнения функции main может быть произвольным,[5] но после успешного (что проверяется использованием assert) завершения scanf они будут содержать два числа, которые удалось последовательно считать со стандартного ввода.

Вариант «дробный»[править]

Рассмотренную выше программу несложно изменить для использования чисел с плавающей запятой.

#include <assert.h>
#include <stdio.h>

int
main ()
{
  double a, b;
  int r = scanf ("%lg%lg", &a, &b);
  assert (r == 2);
  printf ("%lg\n", a + b);
  return 0;
}

Как видно, в этом случае изменяются лишь тип переменных a, b (intdouble) и указатели преобразований (%d%lg.)

Здесь следует отметить, что в случае scanf совершенно идентично будут действовать указатели преобразований %lg, %lf и %le. Напротив, в случае printf не будет разницы между %lg и %g. Причины такого поведения мы также пока оставим без внимания.

Желающим изучить использование других числовых типов в этой задаче предлагается обратиться к разделу «Числовые типы» приложения.

Л-значения и ссылки[править]

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

В некоторых случаях, однако, мы хотим получить от функции что-либо обратно. Иногда, для этого достаточно предусмотренного языком возвращаемого значения. Иначе, можно отправить функции своего рода конверт с обратным адресом (один или несколько). Именно для этого — для получения адреса л-значения — служит унарный оператор &.

Так, запись &a означает адрес ячейки памяти, выделенной для хранения значения переменной a, или же, проще, — ссылка (англ. reference) на переменную a.

Справа от оператора & указывается л-значение (англ. lvalue.[6] Не вдаваясь в подробности отметим, что под л-значением понимают любое выражение, которое может стоять слева от оператора присваивания =. В частности, л-значением является любое выражение, состоящие из имени действительной в данном контексте (англ. scope) переменной.

Для обращения к ячейке памяти по ссылке используется унарный оператор *. Так, выражение *(&a) = 1 полностью равнозначно a = 1.

Вариант «произвольный»[править]

Стандарт C11 требует от реализации языка поддержки числовых типов разрядности 8, 16, 32 и 64 бит.[7] Если по каким-либо причинам в вычислениях требуется бо́льшая разрядность, следует использовать библиотеки операций с числами произвольной разрядности — как, например, GNU MP.[8]

В частности, благодаря GNU MP, пример ниже выведет сумму двух введенных чисел даже если любое из них — или результат сложения — превышает 2⁶⁴.

Код имеет следующие отличия от исходного варианта:

  1. ввод-вывод чисел произвольной разрядности выполняется функциями gmp_scanf и gmp_printfопределенными GNU MP и объявленными в gmp.h; заголовок stdio.h не требуется;
  2. вместо оператора сложения + используется вызов функции mpz_add (a, a, b); в такой форме, действие функции подобно действию выражения a += b для переменных «встроенных» целочисленных типов;
  3. переменные GNU MP требуют обязательной инициализации, которая в примере выполнена функцией mpz_inits ();
  4. тип используемых для вычисления переменных — mpz_t.

#include <assert.h>
#include <gmp.h>

int
main (void)
{
  mpz_t a, b;
  mpz_inits (a, b, 0);
  int r
    = gmp_scanf ("%Zd%Zd", a, b);
  assert (r == 2);
  mpz_add (a, a, b);
  gmp_printf ("%Zd\n", a);

  return 0;
}

Вариант «арифметический»[править]

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

#include <assert.h>
#include <stdio.h>

int
main ()
{
  double a, b;
  int r
    = scanf ("%lg%lg", &a, &b);
  assert (r == 2);
  if (b != 0) {
    printf (("a + b = %lg\n"
             "a - b = %lg\n"
             "a * b = %lg\n"
             "a / b = %lg\n"),
            a + b, a - b, a * b, a / b);
  } else {
    printf (("a + b = %lg\n"
             "a - b = %lg\n"
             "a * b = %lg\n"),
            a + b, a - b, a * b);
  }
  return 0;
}

В этой программе нам потребовался условный оператор контекста утверждения (англ. if statement) — один из четырех (наряду с &&, || и ? :) условных операторов языка. Его синтаксис:[9]

if (выражение)
  тело-если-истинно

if (выражение)
  тело-если-истинно
else
  тело-если-ложно

Где тело-если-истинно и тело-если-ложно могут быть (каждый) единственным утверждением (завершаемым ;), или же, как в примере выше, — { }-блоком.

В случае, если результат вычисления выражения — истина (другими словами — отличен от 0), выполняется тело-если-истинно; в противном случае (и если используется else) — тело-если-ложно.

Заметьте, что каждое простое утверждение (англ. statement) завершается точкой с запятой. Одна из самых популярных синтаксических ошибок начинающих программистов — не ставить точку c запятой после утверждений.

Вариант «тернарный»[править]

Используя условный оператор контекста выражения (англ. conditional operator), а также приняв во внимание тот факт, что printf проигнорирует «избыточные» аргументы (аргументы сверх количества, требуемого указателями преобразований в указанной первым аргументом строке формата), можно незначительно сократить код предыдущего примера.

#include <assert.h>
#include <stdio.h>

int
main ()
{
  double a, b;
  int r
    = scanf ("%lg%lg", &a, &b);
  assert (r == 2);
  printf ((b != 0
           ? ("a + b = %lg\n"
              "a - b = %lg\n"
              "a * b = %lg\n"
              "a / b = %lg\n")
	   : ("a + b = %lg\n"
              "a - b = %lg\n"
              "a * b = %lg\n")),
          a + b, a - b, a * b,
	  (b != 0 ? a / b : 42));
  return 0;
}

В этом варианте, код вновь содержит те же самые строковые константы, что и в предыдущем, однако используется лишь один вызов функции printf — первым аргументом которой (в зависимости от значения переменной b) окажется одна из этих констант.

Число 42, которое передается printf пятым аргументом в случае нулевого значения b, может быть произвольным. Поскольку в этом случае функция printf получает строку формата лишь с тремя указателями формата, фактически использованы будут лишь аргументы с первого (формат) по четвертый (произведение a и b).

Общий синтаксис тернарного оператора ? : следующий:[10]

выражение ? если-истинно : если-ложно

Где выражение вычисляется первым, после чего, если оно истинно (отлично от 0), вычисляется — и принимается результатом оператора в целом — выражение если-истинно; в противном случае — если-ложно.

Примечания[править]

  1. 7.21.6.1 The fprintf function(англ.) WG14 N1570 Committee Draft. ISO/IEC (2011-04-12). Проверено 2012-11-19 г.
  2. 7.21.6.2 The fscanf function(англ.) WG14 N1570 Committee Draft. ISO/IEC (2011-04-12). Проверено 2012-11-19 г.
  3. 7.21 Input/output stdio.h(англ.) WG14 N1570 Committee Draft. ISO/IEC (2011-04-12). Проверено 2012-11-19 г.
  4. 7.2.1.1 The assert macro(англ.) WG14 N1570 Committee Draft. ISO/IEC (2011-04-12). Проверено 2012-11-19 г.
  5. 6.7.9 Initialization(англ.) WG14 N1570 Committee Draft. ISO/IEC (2011-04-12). Проверено 2012-11-19 г.
  6. 6.3.2.1 Lvalues, arrays, and function designators(англ.) WG14 N1570 Committee Draft. ISO/IEC (2011-04-12). Проверено 2012-11-19 г.
  7. 7.20.1.2 Minimum-width integer types(англ.) WG14 N1570 Committee Draft. ISO/IEC (2011-04-12). Проверено 2012-11-19 г.
  8. The GNU MP Bignum Library(англ.) Проверено 2015-04-04 г.
  9. 6.8.4.1 The if statement(англ.) WG14 N1570 Committee Draft. ISO/IEC (2011-04-12). Проверено 2012-11-19 г.
  10. 6.5.15 Conditional operator(англ.) WG14 N1570 Committee Draft. ISO/IEC (2011-04-12). Проверено 2012-11-19 г.