Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Философия программирования на C++ Условия: часть вторая


Здравствуйте, уважаемые подписчики.

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

Код мы приводить опять не будем, лучше будем вставлять рассматриваемые участки.
Первая ошибка является по сути не ошибкой, а примером плохого стиля программирования.
Итак, в приведенном примере числа a, b и c (коэффициенты полинома, надеюсь вы знаете что это) уравнения "a*x^2 + b*x + c = 0" (x^2 - это условная запись x в квадрате) целые... это не есть хорошо, поскольку не имеет значения, целые они или нет. А значит по-хорошему необходимо было бы расширить возможные значения, поэтому рекомендуется использовать тип float или double. Т.е. объявление переменных выглядело бы так:
double a = 1.0;
double b = -12.0;
double c = 27;

Ну остальные переменные просто объявляются для дальнейшего использования (их мы обнуляем). Кстати говоря, новые переменные можно было бы создавать и внутри блока if, но эта возможность будет рассматриваться позже (хотя такое решение более предпочтительно).
В строке
x1 = x2 = -c/b;
сразу 2 ошибки. Во-первых, возможна ошибка деления на 0 – нет проверки переменной b на нулевое значение. Во-вторых, так как переменные c и b целого типа, будет производиться целочисленное деление, что нам не нужно (вы же еще помните что это? :) если нет - посмотрите результат при различных значениях b и c и сразу вспомните). Хотя если опять же переменные объявить нецелыми, то ничего страшного не произойдет.
Следует писать так:
if (b == 0) {    //Если b равен 0
    if (c == 0)    //Если с равно 0
        cout << “Too many solves”;    //Решением является любое х (0*х + 0 = 0)
    else
        cout <<”No solves”;    //Уравнение 0*x + c = 0 не имеет решений
} else {
    x1 = x2 = -c*1.0/b; //Теперь ошибка деления на 0 не произойдет и результат деления будет не целочисленным
    cout << "1 solve. x = "<< x1<< endl;
}

Напомним, что результатом деления будет целое число (частное без остатка), если оба операнда являются целыми. Поэтому в примере выше если мы умножим на 1.0 (которое является типом double, с плавающей точкой), то в результате слева от деления будет тип double, а значит результатом деления будет он же (а не целое число).
Соответственно следующая проверка становится излишней (этот случай рассматривался выше):
if( (a == 0) && (b == 0) && (c == 0) )
    cout << "Too many solves" << endl;

Поэтому мы ее просто уберем из кода. Более того проверка на равенство нулю коэффициентов b и с не нужна, если а не равно нулю (математику ведь все знают? :)). Поэтому код
if (b == 0)//ур-е a*x^2+c= 0;
    if (c < 0){
        x1 = sqrt(c);//sqrt - квадратный корень        x2 = -sqrt(c);    } else
        cout << "No solve";

также является лишним. Но и тут имеется довольно глупая ошибка :) Проверяем число на отрицательность, потом находим от него же корень. А действительного корня от отрицательного числа, как известно, не существует (мы не рассматриваем комплексные решения). На самом деле код должен был быть следующим, просто забыт минус (а если быть еще более точным - кода этого вообще не нужно).
if (b == 0)//ур-е a*x^2+c= 0;
    if (c < 0){
        x1 = sqrt(-c);//sqrt - квадратный корень
        x2 = -sqrt(-c);
    }

Это является довольно частой ошибкой (впрочем, легко обнаруживаемой, а потому неопасной, деление на ноль выше куда опаснее).
Ну дальше имеется ошибка в строке x1 = x2 = -b/(2 * a); аналогичная описанной выше (сами догадаетесь? :)).
Теперь замечание по оформлению. Код написан в целом правильно, со всем требующимися отступами. Да, отступы это хорошо... но ведь если вся программа будет написана справа, а слева будут одни пробелы, это будет выглядеть уже не так хорошо :). В общем, всего хорошего в меру. Кроме того можно снизить их число (и только выиграть от этого) при написании else if. В этом случае код читается проще. Ну например, многие пишут так:
if(a == 0) {
    //.... Тут какой-то код
} else if(b == 0) {
        //.... Тут какой-то код - с двойным отступом слева(!)
    }

Внутри второго if этот отступ явно лишний :) Кстати говоря, функция sqrt вычисляет квадратный корень (она объявлена в заголовочном файле <cmath>, об этом позже).
Теперь, как и обещали, приведем пример программы с верным алгоритмом.
#include <iostream>
#include <cmath>    //подключаем функции математики
using namespace std;
int main() {
    double a = 1;
    double b = -12;
    double c = 27;
    double D = 0, x1 = 0, x2 = 0;
    if (a == 0) {    //Решаем уравнение b*x + c = 0
        if (b == 0) {    //Если b равен 0
            if (c == 0)    //Если с равно 0
                cout << “Too many solves”;    //Решением является любое х (0*х + 0 = 0)
            else
                cout <<”No solves”;    //Уравнение 0*x + c = 0 не имеет решений
        } else {
            x1 = x2 = -c/b; //Теперь ошибка деления на 0 не произойдет, так как b здесь не может быть равным 0, и результат деления будет не целочисленным
            cout << "1 solve. x = " << x1 << endl;
        }
    } else {    //О приоритетах операций сравнения будет коротко рассказано в следующем выпуске, а пока что знаний математики должно быть достаточно
        D = b*b - 4*a*c;    //вычисляем дискриминант
        if (D < 0) {    //Зачем тут фигурная скобка если дальше одна команда? Да просто так смотрится гораздо лучше :)
            cout << "No solves";    //Нет решений при отрицательныом дискриминанте
        } else if (D == 0) {    //Дискриминант равен нулю, значит имеется единственное решение
            x1 = x2 = -b/(2*a);    //Вот кстат пример неувеличения отступа при else if
            cout << "1 solve. x = " << x1 << endl;
        } else {
            x1 = (-b + sqrt(D)) / (2 * a);
            x2 = (-b - sqrt(D)) / (2 * a);
            cout << "2 solves. x1 = " << x1 << ", x2 = " << x2 << endl;
        }
    }
}

Ну и под конец выпуска вкратце расскажем об операторе switch (аналог goto, в отличие от которого довольно часто используется, кстати говоря об операторе goto речи идти не будет, он просто никому не нужен и пользуется дурной славой).
В предыдущем выпуске вы увидели, как просто проверять с помощью конструкции if или if…else верность условия. Но, допустим, надо вывести наименование цифры. Для 1 – вывести "one", 2 – "two", 3 – "three" и т.д. Если делать это с помощью if, то придется расписать 10 идущих подряд проверок (кстати говоря здесь else не нужен... подумайте почему).
#include <iostream>
using namespace std;
int main() {
    int x = 1;    //Пока я еще не рассматривал ввода чисел с клавиатуры, поэтому задавать x необходимо тут, и после этого перекомпилировать программу
    if (x == 0)
        cout << "Zero";
    if (x == 1)
        cout << "One";
    if (x == 2)
        cout << "Two";
    if (x == 3)
        cout << "Three";
    //Ну тут сами остальное допишите...
    if (x == 9)
        cout << "Nine";
}

Часть проверок if мы опустили. Очевидно, что такой подход не очень эффективен (в смысле наглядности кода). Конечно тут все понятно, но все равно не хотелось бы писать столь много проверок.
В случаях, подобных приведенному выше, используется конструкция switch.
Её синтаксис:
switch (переменная){
    case значение1: команда; break;
    case значение2: команда; break;
    //...
    default: команда;
}

Значение переменной проверяется на совпадение с одним из значений, перечисленных в switch, и если совпала – выполняется команда, стоящая после совпавшего значения. Если значение переменной ни с чем не совпало – выполняются команды блока default. Наличие default необязательно, но желательно (если его нету, то программа просто выходит из блока switch и продолжает работу). Как и в случае с if, здесь под командой понимается либо один оператор, оканчивающийся точкой с запятой, либо последовательность таких одиночных операторов (кусок кода). Отметитм, что в данном случае фигурные скобки для команды необязательны. Вообще говоря switch является аналогом оператора goto (который просто без всяких условий переводит выполнение программы на определенную строку). Если значение переменной совпало с каким-либо из значений case, то программа продолжает выполняться с этого места. Поэтому необходим оператор break; после команды, с его помощью программа узнает, что необходимо прервать выполнение внутри блока switch и продолжить выполнение программы вне его. Чтобы лучше понять зачем он нужен - просто уберите его :).
Итак, перепишем вывод цифр с помощью switch.
#include <iostream>
using namespace std;
int main() {
  int x = 5;
  switch (x){
    case 0: cout << "Nol"; break;
    case 1: cout << "Odin"; break;
    case 2: cout << "Dva"; break;
    case 3: cout << "Tri"; break;
    case 4: cout << "Chetyre"; break;
    case 5: cout << "Pyat"; break;
    case 6: cout << "Shest"; break;
    case 7: cout << "Sem"; break;
    case 8: cout << "Vosem"; break;
    case 9: cout << "Devyat"; break;
    default: cout << "Ne chislo ot 0 do 9"; break;
  }
}

Как видите, программа стала гораздо нагляднее.
PS. Еще немного слов об оформлении. В последнем примере отступы были сделаны с помощью двух пробелов. Как правило есть 2 основных подхода: отступ в виде 4 пробелов или (как я делал выше) в виде табуляции. Что использовать - решать вам. Я исхожу из того, что проще нажать одну кнопку Tab, чем 4 раза пробел :). Да и навигация по коду становится проще на мой взгляд.
И об использовании английских наименований. Согласитесь, гораздо лучше выглядит "Nine", чем транслит "Devyat". Я бы все-таки рекомендовал придерживаться обычного моего подхода - все делать на английском языке, без транслитов (так проще читать код).

На этом все. До следующего выпуска! Все свои вопросы и комментарии вы можете присылать на почту asm_89@mail.ru

В избранное