6

Помогите, как можно улучшить программу по нахождению корней квадратного уравнения.

#include<iostream>
#include<cmath>

using namespace std;

double dis(double a, double b, double c);//Псевдоним функции по нахождению дискриминанта

int main()// main функция
{
   double a, b, c, xa, xb, disb;//переменные, необходимые для программы
   cout << "Введите коэфиециент а: ";// Следующие строчки это сбор данных
   cin >> a;
   cout << a << endl;
   cout << "Введите коэфиециент b: ";
   cin >> b;
   cout << b << endl;
   cout << "Введите свободное слагаемое с: ";
   cin >> c;
   cout << c << endl;
   cout << "КОНЕЧНЫЙ ВИД: " << a << "x^2" << "+(" << b << ")" << "x" << "+(" << c << ")" << "=0" << endl;//выводит конечный вид уравнения
   double ld;//переменная, в которую будет записан возвращаемый результат функции dis()
   ld = dis(a, b, c);// запись
   if (ld < 0)//ветвление для защиты от отрицательного дискриминаната
   {
      cout << "Ой, все, решений нет." << endl;
   }
   else
   {
      disb = sqrt(ld); //вычисляем квадратный корень
      xa = ((-b) + disb) / (2 * a);// находим корни
      xb = ((-b) - disb) / (2 * a);
      cout << "ПЕРВЫЙ Х РАВЕН: " << xa << endl;//выводим корни
      cout << "ВТОРОЙ Х РАВЕН: " << xb << endl;
   }
}

double dis(double a, double b, double c)// функця по нахождению дискриминанта
{
   double xld;
   xld = ((b*b) - (4 * a*c));
   cout << "Дискриминант равен: " << xld << endl;
   return xld;
}
rdorn
  • 16,323
  • 1
    Я приклеил соответствующую метку, рекомендую прочесть её описание. Возможно, вы захотите немножко дополнить/поправить вопрос. –  Feb 15 '16 at 16:24
  • Речь идет о C++ или простом С? Потому что пока тут от плюсов только использование cin и cout – newman Feb 15 '16 at 16:26
  • Newman, это C++ – Aleksandr Dremov Feb 15 '16 at 16:29
  • @Alexander Тут даже переменные объявлены, как в С - сразу скопом в начале функции... От C++ тут только потоки ввода-вывода. Кстати, зачем после ввода переменной вы сразу же ее выводите?

    Но я хотел задать другой вопрос - ладно с дискриминантом, но если кто-то введет a=0, что получится?.. а что должно? :)

    – Harry Feb 15 '16 at 16:33
  • Если а будет 0, то просто первое слагаемое сократится и будет неполное квадратное уравнение, но ответы будут, причем правильные, если подставить. – Aleksandr Dremov Feb 15 '16 at 16:37
  • @AlexanderDremov, это был намек, что вот тут: xa=((-b)+disb)/(2*a); будет деление на 0 – Grundy Feb 15 '16 at 16:38
  • Черт, не заметил, еще ветвление надо делать, если a - 0, то x = c/b – Aleksandr Dremov Feb 15 '16 at 16:40
  • @AlexanderDremov на самом деле, x = -c/b в таком случае. – StateItPrimitive Feb 15 '16 at 17:02
  • Да, уже исправил if (a==0)
    { xa=(-c/b); cout<<"Линейное уравнение. Ответ : "<<xa<<endl; }
    – Aleksandr Dremov Feb 15 '16 at 17:04
  • Решение квадратного уравнения - одна из задач, которая может сильно страдать от особенностей поведения плавающей арифметики. Причем это "страдание" можно существенно победить, если делать вычисления немножко хитрее. В частности, на эту тему есть старая статья George Forsythe http://i.stanford.edu/pub/cstr/reports/cs/tr/66/40/CS-TR-66-40.pdf – AnT stands with Russia Feb 15 '16 at 21:02
  • Формально это С++, но от С++ в этом коде, конечно, одно название – AnT stands with Russia Feb 15 '16 at 21:03
  • Если кто-нибудь своим ответом-таки решил вашу проблему, то не поленитесь отметить ответ "галочкой" (рядом со "стрелочками" у ответов), чтобы он не висел в списке неотвеченных :) – StateItPrimitive Feb 24 '16 at 22:17

4 Answers4

15

Программу можно "улучшить" так: решать не так в лоб, как когда-то в школьной тетрадке писали, а с учетом особенностей поведения компьютерной плавающей арифметики. В частности, существенно лучшим в этом отношении является следующий подход к решению

double d = b * b - 4 * a * c; // Дискриминант

double q = b >= 0 ? (-b - sqrt(d)) / 2  : (-b + sqrt(d)) / 2; 
// Здесь узнается наше родное `-b +- sqrt(d) / 2 * a`, 
// но пока что без `a` в знаменателе

double x1 = q / a;
double x2 = c / q;

Те, кто знаком с формулами Виета, легко увидят и математическую правильность решения. А вот почему следует поступать именно так, можно почитать в классической статье (а на русском языке - у Моулера, Малькольма и того же Форсайта в книге "Машинные методы математических вычислений")

Вкратце, идея заключается в том, что в плавающей арифметике во избежание потери точности рекомендуется избегать сложения относительно больших чисел, близких по абсолютному значению, но имеющих разные знаки - результат может получиться катастрофически неточным. В "лобовом" решении подвыражение -b - sqrt(d) может страдать от этой проблемы, если b отрицательно. Вышеприведенный подход при вычислении промежуточной величины q всегда выполняет сложение чисел с одинаковыми знаками.

8
#include<iostream>
#include<cmath>

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

using namespace std;

Никогда не пишите using namespace std;, особенно в глобальном пространстве имен. Это приводит к конфликтам имен, причем иногда ошибки компиляции не будет, но программа будет вызывать какую-то стандартную функцию вместо Вашей.

double dis(double a, double b, double c);//Псевдоним функции по нахождению дискриминанта

Это называется "декларация" или объявление функции, а не псевдоним. Нет никакого смысла ее тут использовать. Пишите свои функции в начале, в main() в конце программы.
Не надо экономить на буквах. Переименуйте в discriminant.

int main()// main функция

No shit bro. Не надо писать бессмысленные коментарии, и так понятно что она main и что она функция.

{
   double a, b, c, xa, xb, disb;//переменные, необходимые для программы

Не надо объявлять несколько несвязанных переменных в одной декларации (ES10). Объявляйте переменные только в месте их использования (ES21) и всегда их инициализируйте (ES20) (если есть чем).

   cout << "Введите коэфиециент а: ";// Следующие строчки это сбор данных

Не надо писать комментарии, пишите код. Вынесите это в функцию void collect_input(double& a, double& b, double& c) или лучше Coefficients read_coefficients(std::istream& stream), тогда название функции будет говорить о том что тут делается.

   cin >> a;
   cout << a << endl;

Здесь не нужен endl, используйте '\n'.

   cout << "Введите коэфиециент b: ";
   cin >> b;
   cout << b << endl;
   cout << "Введите свободное слагаемое с: ";
   cin >> c;
   cout << c << endl;
   cout << "КОНЕЧНЫЙ ВИД: " << a << "x^2" << "+(" << b << ")" << "x" << "+(" << c << ")" << "=0" << endl;//выводит конечный вид уравнения

Не надо писать бессмысленные комментарии, в сообщении уже написано что это.
Сложный код лучше выносить в отдельные функции, например print_formula(a, b, c);

   double ld;//переменная, в которую будет записан возвращаемый результат функции dis()
   ld = dis(a, b, c);// запись

Должно быть double dis = discriminant(a, b, c);. Почему вообще ld? Не используйте непонятные сокращения. Также лучше использовать вывод типов: auto dis = ...;

   if (ld < 0)//ветвление для защиты от отрицательного дискриминаната
   {
      cout << "Ой, все, решений нет." << endl;

Не надо мешать в одну кучу вычисление корней уранения, обработку случая отсутствия корней и логирование ошибок.
Вынесите вычисление корней в отдельную функцию, например bool try_solve(const Coefficients& a, Roots& x). Альтернативный вариант - std::vector<double> solve(const std::vector<double>& coeffs).

   }
   else
   {
      disb = sqrt(ld); //вычисляем квадратный корень
      xa = ((-b) + disb) / (2 * a);// находим корни
      xb = ((-b) - disb) / (2 * a);

Должно быть auto xa = ...; или double xa = ...;

      cout << "ПЕРВЫЙ Х РАВЕН: " << xa << endl;//выводим корни
      cout << "ВТОРОЙ Х РАВЕН: " << xb << endl;
   }
}

double dis(double a, double b, double c)// функця по нахождению дискриминанта
{
   double xld;
   xld = ((b*b) - (4 * a*c));
   cout << "Дискриминант равен: " << xld << endl;
   return xld;
}

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

struct QuadraticEquation {
  QuadraticEquation(double a, double b, double c) : a(a), b(b), c(c) {}

  bool solve() {
    discr = calc_discriminant();
    if (discr < 0) return false;
    x1 = ...
    return true;
  }

  double a, b, c;
  double discr;
  double x0, x1;
};

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

Abyx
  • 31,143
  • "Никогда не пишите using namespace std;", "Не надо объявлять несколько несвязанных переменных в одной декларации ..." - не согласен. – Qwertiy Feb 15 '16 at 22:57
  • 1
    @Abyx Вот принцип, по которому using namespace не пишется в хедерах весьма очевиден, а вот в cpp-шниках не столь понятен. Ведь люди сознательно вставляют такие конструкции в cpp-шники чтобы избежать многословности в тех случая, когда функции из разных пространств имен не пересекаются, контролируя последствия, ну, или по крайней, мере, люди с опытом контролируют :) – StateItPrimitive Feb 16 '16 at 08:33
4

На мой взгляд, имеет смысл:

  1. Вынести функцию решения квадратного уравнения:

    int solve(double a, double b, double c, double *x1, double *x2)
    

    a, b, с - коэффициенты
    возвращаемое значение - количество корней (или -1 если их бесконечно много)
    *x1, *x2 - сами корни (если корней меньше двух, лишние переменные не трогаем)

  2. Сравнивать дробные числа с некоторой точностью EPS, например, 1e-7.
    В том числе, сравнить дискриминант с нулём как fabs(d) < EPS.

  3. Убрать функцию вычисления дискриминанта.

  4. Ввод и вывод оставить в main'е.

Qwertiy
  • 123,725
  • @Abyx, чтобы визуально было заметно, что туда делается запись. Но можно и ссылки - это на усмотрение пишущего :) – Qwertiy Feb 15 '16 at 22:34
  • @Abyx, при вызове. n = f(a, b, c, x1, x2) или n = f(a, b, c, &x1, &x2) - где заметнее, что здесь out? – Qwertiy Feb 15 '16 at 22:59
  • @Abyx, и все же (imho, конечно же) ссылки наряду с перегрузкой функций это (при чтении чужого кода) неприятные вещи в крестах. – avp Feb 15 '16 at 23:56
1

Код с минимальными правками (подправил алгоритм):

#include <iostream>

using namespace std;

const double EPS = 1E-5;

static bool isEqual(const double& left, const double& right, const double& eps = EPS)
{
   return abs(left - right) < EPS;
}

double dis(double a, double b, double c);//Псевдоним функции по нахождению дискриминанта

int main()// main функция
{
   double a, b, c, xa, xb, disb;//переменные, необходимые для программы
   cout << "Введите коэфиециент а: ";// Следующие строчки это сбор данных
   cin >> a;
   cout << a << endl;
   cout << "Введите коэфиециент b: ";
   cin >> b;
   cout << b << endl;
   cout << "Введите свободное слагаемое с: ";
   cin >> c;
   cout << c << endl;
   cout << "КОНЕЧНЫЙ ВИД: " << a << "x^2" << "+(" << b << ")" << "x" << "+(" << c << ")" << "=0" << endl;//выводит конечный вид уравнения
   double ld = dis(a, b, c);//переменная, в которую будет записан возвращаемый результат функции dis()
   if (ld < 0.0)//ветвление для защиты от отрицательного дискриминаната
   {
      cout << "Ой, все, решений нет." << endl;
   }
   else if (isEqual(0.0, a))
   {
      if (isEqual(0.0, b))
      {
         if (isEqual(0.0, c))
            cout << "Существует бесконечное множество решений." << endl;
         else
            cout << "Ой, все, решений нет." << endl;
      }
      else
         cout << "Х РАВЕН: " << -c/b << endl;
   }
   else
   {
      disb = sqrt(ld); //вычисляем квадратный корень
      if (isEqual(0.0, b))
      {
         cout << "Х РАВЕН: " << disb / (2*a) << endl;
      }
      else
      {
         xa = (-b + disb) / (2*a);// находим корни
         xb = (-b - disb) / (2*a);
         cout << "ПЕРВЫЙ Х РАВЕН: " << xa << endl;//выводим корни
         cout << "ВТОРОЙ Х РАВЕН: " << xb << endl;
      }
   }
}

double dis(double a, double b, double c)// функця по нахождению дискриминанта
{
   double xld;
   xld = b*b - 4*a*c;
   cout << "Дискриминант равен: " << xld << endl;
   return xld;
}

Если вам не принципиально выдавать доп. информацию для пользователя в функции dis, то сразу напишите там return b*b - 4*a*c; во благо срабатывания RVO, либо вообще откажитесь от данной функции (ведь вы её используете всего 1 раз), ну, либо можете извратиться и запехнуть это в макрос, например #define dis(a, b, c) b*b - 4*a*c.
Ну и всю логику решения (именно решения задачи, а не ввода данных), по-моему, неплохо бы вынести в отдельную функцию хотя бы.


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

  • 3
    Ну и зачем тут ООП??? – Qwertiy Feb 15 '16 at 17:49
  • 1
    @Qwertiy ну ведь жутко же неудобно, когда взаимосвязанные функции просто разбросаны по коду или у вас другой взгляд на данный вопрос? – StateItPrimitive Feb 15 '16 at 17:50
  • 1
    Другой. Нужна одна функция для решения уравнения. Всё. – Qwertiy Feb 15 '16 at 18:15
  • @Qwertiy т.е. вы обычно не рефакторите свой код? – StateItPrimitive Feb 15 '16 at 18:22
  • Тогда уж все внутренние функции оставить приватными, наружу выставить метод solve без параметров и вставить возможность возврата комплексных корней, т.к. решение есть всегда и их всегда 2. Это если полностью в ООП стиле. А так, излишества все это. – rdorn Feb 15 '16 at 18:24
  • 1
    Не помню, как оно точно звучало, но примерно так: "можно написать простой код, в котором, очевидно, нет ошибок; а можно сложный, в котором нет очевидных ошибок" ;) – Qwertiy Feb 15 '16 at 18:53
  • 1
    И вообще, а где в твоём коде ООП? Единственный класс, все методы которого статические. Причём, этот класс не только решает уравнение, но и занимается вводом-выводом. И он не просто совмещает эти обязанности, но и совмещает их таким образом, что использование класса для решения уравнения без ввода коэффициентов пользователем, или без вывода ответа невозможно. – Qwertiy Feb 15 '16 at 18:55
  • А еще можно фабрику по производству фабрик молотков прикрутить... Квадратное уравнение... – andy.37 Feb 15 '16 at 19:01
  • @andy.37, было бы не плохо, прикрутите? ;-) – Grundy Feb 15 '16 at 19:01
  • @Grundy, староват я для этого))) – andy.37 Feb 15 '16 at 19:02
  • @Abyx, запилите что-то функционально? :) – Grundy Feb 15 '16 at 19:08
  • Да ну вас нафиг, хотел просто помочь человеку, а вы демагогию развели, да еще и засрали тут все что можно :) – StateItPrimitive Feb 15 '16 at 20:09
  • @Abyx Ок, я вас понял, сейчас напишу как требуют желающие :) – StateItPrimitive Feb 15 '16 at 20:32
  • 3
    По-моему, вы ошиблись в самом начале. [tag:инспекция-кода] не про "отрефакторьте мне код", а про критику: комментарии к строчкам и структуре предложенного кода и пояснения, что там стоит поправить, как это называется и почему. А так вы предоставили свой вариант кода, и [tag:инспекция-кода] началась уже над вашим кодом, а код в вопросе остался почти без внимания :} –  Feb 15 '16 at 21:12
  • 1
    @D-side Теперь и я это понял и приму к сведению, а то тролли не дремлют :) – StateItPrimitive Feb 15 '16 at 21:17
  • 1
    @D-side, а может под инспекцией кода каждый понимает все же что-то свое, и не факт, что Ваше понимание должно быть истиной для всех. – avp Feb 16 '16 at 22:36
  • @avp возможно, но моё понимание основано на наблюдении за CodeReview.SE. Да и голоса за другие ответы к этому же вопросу это подтверждают. –  Feb 17 '16 at 08:18