Самостоятельная работа 3: элементарные вычисления

Кувшинов Д.Р.

2015


Общее оглавление


8 баллов

Цели и критерии

Цели: изучение средств для выполнения элементарных вычислений на языке C++, части содержимого стандартного заголовочного файла cmath — соответствие между простейшими математическими формулами и программами; изучение синтаксиса конструкций выбора альтернатив (if-else, switch-case, “тернарного оператора”), некоторых способов возвращения более одного значения из функции.

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

Критерии полноты

  1. Реализована функция, вычисляющая f(x, a, b, c). Упрощение выражения f не допускается.
  2. Реализована функция вычисления (одного) корня для параметров (a, b, c).
  3. Функция вычисления корня проверяет все возможные случаи несуществования корней и существования несчётного множества корней (например, когда подходит любое действительное число). Допустимы некоторые послабления — см. пример.
  4. Реализована программа, позволяющая пользователю ввести произвольное количество наборов значений параметров (a, b, c) и для каждого набора выводящая либо некоторый корень, либо констатацию факта несуществования корня, либо констатацию факта существования несчётного множества корней.
  5. При обнаружении корня, программа должна выполнять подстановку его и исходных параметров в f(x, a, b, c) и выводить полученное значение, которое должно быть близко нулю, что является проверкой корректности решения.


Синтаксическая справка

Перед выполнением задания рекомендуется изучить справочный материал по числам с плавающей точкой и стандартным функциям, определённым в заголовочном файле cmath.

Инструкция if-else

Ключевые слова if и else позволяют организовать последовательную проверку группы альтернативных условий и выполнение действия при выполнении некоторого из этих условий или невыполнения всех условий (ветка else).

if ( /* поместите сюда условие, круглые скобки вокруг условия обязательны */ )
{
  // код, который выполнится в случае истинности условия
}
else // else-ветка не является обязательной
{
  // код, который выполнится в случае ложности условия
}

Пример каскадного if-else.

if (day == 1)
  cout << "понедельник";
else if (day == 2)
  cout << "вторник";
else if (day == 3)
  cout << "среда";
else if (day == 4)
  cout << "четверг";
else if (day == 5)
  cout << "пятница";
else if (day == 6)
  cout << "суббота";
else if (day == 7)
  cout << "воскресенье";
else
  cout << "(неверный номер дня недели)";

Инструкция switch-case

Предыдущий пример каскадного if-else относится к случаям применимости инструкции switch-case, позволяющей перейти на метку case с целочисленным значением, равным значению выражения, указанного в круглых скобках после ключевого слова switch. Ключевое слово default задаёт метку, переход на которую осуществляется, если среди case-случаев нет подходящей константы. Так как выполнение внутри switch просто продолжается с соответствующей метки, часто требуется ставить ключевое слово break для явного выхода из конструкции switch-case.

switch (day)
{
case 1:
  cout << "понедельник";
  break;
case 2:
  cout << "вторник";
  break;
case 3:
  cout << "среда";
  break;
case 4:
  cout << "четверг";
  break;
case 5:
  cout << "пятница";
  break;
case 6:
  cout << "суббота";
  break;
case 7:
  cout << "воскресенье";
  break;
default:
  cout << "(неверный номер дня недели)";
}

Попробуйте убрать часть break и выполнить этот код для разных значений переменной day.

С практической точки зрения приведённый выше код логичнее выразить в виде отдельной функции, которая “переводит” номер дня в строку (название дня). Куда её выводить (в cout или нет) пусть решает пользователь функции. Использование return делает break избыточными.

const char* day_of_week(unsigned day)
{
  switch (day)
  {
  case 1:
    return "понедельник";
  case 2:
    return "вторник";
  case 3:
    return "среда";
  case 4:
    return "четверг";
  case 5:
    return "пятница";
  case 6:
    return "суббота";
  case 7:
    return "воскресенье";
  default:
    return "(неверный номер дня недели)";
  }
}

Оператор ?:

Оператор ?:, также известный как “тернарный оператор” (благодаря тому, что принимает три операнда), позволяет включить выбор одного значения из двух по логическому условию непосредственно в выражение.

/* условие */ ? /* значение в случае истинности */ : /* значение в случае лжи */

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

Простой пример: реализация функции “модуль числа”. Через if-else:

double abs(double x)
{
  if (x < 0.0)
    return -x;
  else
    return x;
}

С помощью тернарного оператора её можно записать короче:

double abs(double x)
{
  return x < 0.0? -x: x;
}

Тернарный оператор допускает вложение. Например, функция “сигнум” (знак числа):

int sgn(double x)
{
  return x < 0.0? -1:
         x > 0.0? +1:
         0;
}

Тернарный оператор можно применить, чтобы заменить switch в примере с функцией day_of_week выше на выборку из массива. Это не означает, что подобная замена всегда удобна или практически осмысленна — иначе бы не было в языке инструкции switch. Здесь важен тот факт, что перебираемые значения занимают сплошной диапазон 1–7.

const char* day_of_week(unsigned day)
{
  // Заранее заданный массив значений.
  static const char* name[8] =
  {
    "(неверный номер дня недели)",
    "понедельник",
    "вторник",
    "среда",
    "четверг",
    "пятница",
    "суббота",
    "воскресенье"
  };

  return name[day < 8? day: 0];
}

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

// Описание "культуры": национальные и языковые особенности.
struct Culture
{
  // Названия дней недели.
  const char* day_of_week[8];
  // ...
};

const char* day_of_week(unsigned day, const Culture &culture)
{
  return culture.day_of_week[day < 8? day: 0];
}


Примеры

Линейное уравнение

Пусть дано линейное уравнение ax + b = 0, a и b из ℝ. Как можно оформить функцию C++, решающую это уравнение для произвольной пары параметров a, b?

double solve_linear(double a, double b)
{
  // Неужели так просто?
  return -b / a;
}

Можно поэкспериментировать с этим кодом, вводя разные a и b. Интересно, что произойдёт, если ввести a = 0? А если и b = 0?

Очевидно, что решений у конкретного уровнения с конкретными значениями параметров может и не быть, а может быть бесконечно много (случай a = 0 и b = 0). Простейшим подходом будет заявить “наша функция возвращает решение уравнения, если оно существует и единственно”, но это означает перекладывание задачи по определению возможности решения уравнения на пользователя программы. В то же время, мы же решаем уравнение? Кому как не нам в таком случае разрешать возможные “плохие случаи”? Поэтому более удобной для пользователя будет функция, которая проверяет параметры и помимо корня способна возвращать описание ситуации (например, “корень получен”, “корней не существует”, “(почти) любое число подходит в качестве корня”). Этого можно добиться, возвращая числовой код ситуации, либо характеристику “количество корней”. При этом будем толковать отрицательное количество как ситуацию “почти любое число подходит в качестве корня”.

Однако, если функция возвращает количество корней, то нужно решить каким образом она будет возвращать одновременно и сам корень, если он существует… C++ предлагает несколько способов решения данной проблемы. Предпочтительный способ зависит от конкретной ситуации. Мы выберем простейший с точки зрения требуемого объёма знаний способ — передачу переменной-получателя дополнительного значения в функцию по ссылке. Синтаксически это оформляется дописыванием знака “амперсанд” & после типа параметра, передаваемого по ссылке.

// Передача параметра по значению.
void by_value(int n)
{
  // n -- локальная переменная функции by_value
  n = 42; // это изменение не выйдет за пределы вызова
}

// Передача параметра по ссылке.
void by_reference(int &n)
{
  // n -- псевдоним внешней переменной
  n = 23; // это изменение внешней относительно функции переменной
}

// Тест.
int main()
{
  int x = 0;
  by_value(x);
  cout << x; // > 0
  by_reference(x);
  cout << x; // > 23
}

Теперь вы можете вернуть из функции сразу несколько значений “за один раз”, но под них придётся заранее завести переменные. Функцию, решающую линейное уравнение перепишем так, чтобы в качестве основного результата она возвращала количество корней (тогда его удобно будет сразу проверять в условии инструкций if или switch), а вычисленный корень записывала в переменную, переданную по ссылке в качестве параметра.

// Особое значение "бесконечное количество корней".
const int INFINITE_ROOTS = -1;

// Функция возвращает "количество корней".
// Корень записывает по ссылке root.
int solve_linear(double a, double b, double &root)
{
  if (a == 0)
    return b == 0? INFINITE_ROOTS: 0;
  root = -b / a;
  return 1;
}

C++, HTML


Квадратное уравнение

Усложним задачу. Будем решать произвольное квадратное уравнение ax2 + bx + c = 0. Все коэффициенты могут принимать любые значения, соответственно, у нас может быть 0, 1, 2 корня или все действительные числа могут быть корнями. При этом случай a = 0 мы уже умеем решать с помощью функции solve_linear. И как раз удобно, что она возвращает количество корней, поэтому можно просто записать вызов solve_linear в инструкции return.

Итак, функция solve_quadratic возвращает до двух корней, записывая их в переменные, переданные по ссылкам. При этом root2 (второй корень) записывается только в случае наличия двух разных корней.

int solve_quadratic(double a, double b, double c, double &root1, double &root2)
{
  if (a == 0) // сводится к линейному
    return solve_linear(b, c, root1);
  // ниже a != 0

  const double d = b * b - 4.0 * a * c;
  if (d < 0) // нет корней
    return 0;
  if (d == 0) // один корень
  {
    root1 = -b / (2.0 * a);
    return 1;
  }

  // два корня
  const double ds = sqrt(d);
  root1 = (-b - ds) / (2.0 * a);
  root2 = (-b + ds) / (2.0 * a);
  return 2;
}

C++, HTML


Ключевое слово const (константа) следует использовать в тех случаях, когда значение создаваемой переменной не предполагается изменять. Да, формально это всё равно “переменная”, хотя и считается, что после инициализации её значение неизменно. Компилятор постарается проверить, пытаемся ли мы изменить значение этой переменной, и выдаст ошибку компиляции, если обнаружит такую возможность. Эта ошибка компиляции будет означать ошибку в наших рассуждениях или коде. Кроме того, const часто помогает компилятору производить более эффективный машинный код.


Тригонометрическое уравнение

Рассмотрим пример, напоминающий варианты из задания. Пусть f(x, a, b, c) = 1 + sin(ax + |logb c|). Положим f(x) = 0 и формально выразим x через значения параметров a, b, c. Получим x = loga (–|logb c| – π/2 + 2π n), где n ∈ ℤ (здесь достаточно взять одну из веток решений).

Анализируя полученную формулу, выражающую x, заключаем следующее.

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

// Решение уравнения f(x) = 0.
int solve_f(double a, double b, double c, double &root)
{
  if (a < 0.0 || b <= 0.0 || b == 1.0 || c <= 0.0)
    return 0; // нет корней
  if (a == 0.0 || a == 1.0) // потенциально почти все возможные x -- корни
    return is_almost_zero(f(a, b, c, 1))? INFINITE_ROOTS: 0;

  // Счётное число корней, получим один из них.
  const double
    expr_part = abs(log(b, c)) + Half_Pi,  // часть выражения
    n = 1.0 + ceil(expr_part / Double_Pi), // номер корня
    log_arg = Double_Pi * n - expr_part;   // аргумент логарифма в формуле корня

  assert(log_arg > 0.0); // всегда должен быть положительным по построению
  root = log(a, log_arg);
  return 1;
}

Здесь INFINITE_ROOTS уже является признаком не просто бесконечного числа корней, но несчётно большого числа корней (например, все действительные числа кроме нуля), это особые “вырожденные случаи”.

Используемая в тексте функция is_almost_zero — вспомогательная функция, проверяющая значение на близость нулю. Она заменяет проверку на равенство, которая может плохо работать на практике из-за погрешности машинных вычислений. Функция is_almost_zero определена следующим образом (tolerance — допустимое значение отклонения значения от нуля).

// Проверка значения на близость нулю.
bool is_almost_zero(double x, double tolerance = Tolerance)
{
  return abs(x) <= tolerance;
}

Кроме is_almost_zero в коде определена функция log двух параметров (в стандарте такой нет), вычисляющая логарифм по произвольному основанию как отношение натуральных логарифмов.

C++, HTML


Варианты

Стандартные математические функции

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17


Общее оглавление

Кувшинов Д.Р. © 2015