Программирование. Принципы и практика использования C++ Исправленное издание - Бьёрн Страуструп
Шрифт:
Интервал:
Закладка:
void Axis::set_color(Color c)
{
Shape::set_color(c);
notches.set_color(c);
label.set_color(c);
}
Аналогично, функция Axis::move() перемещает все три части объекта класса Axis одновременно.
void Axis::move(int dx, int dy)
{
Shape::move(dx,dy);
notches.move(dx,dy);
label.move(dx,dy);
}
15.5. Аппроксимация
Рассмотрим еще один небольшой пример построения графика функции: “анимируем” вычисление экспоненты. Наша цель — дать вам почувствовать математические функции, продемонстрировать применение графиков для иллюстрации вычислений, показать фрагменты кода и, в заключение, предупредить о типичных проблемах, связанных с вычислениями.
Один из способов вычисления экспоненты сводится к суммированию степенного ряда.
ex = 1 + x + x2/2! + x3/3! + x4/4! + ...
Чем больше членов ряда мы вычислим, тем точнее будет значение ex; иначе говоря, чем больше членов ряда мы вычисляем, тем больше правильных цифр найдем в результате. В программе мы суммируем ряд и строим график его частичных сумм. В этой формуле знак восклицания, как обычно, обозначает факториал, т.е. мы строим графики функций в следующем порядке:
exp0(x) = 0 // нет членов
exp1(x) = 1 // один член
exp2(x) = 1+x // два члена ; pow(x,1)/fac(1)==x
exp3(x) = 1+x+pow(x,2)/fac(2)
exp4(x) = 1+x+pow(x,2)/fac(2)+pow(x,3)/fac(3)
exp5(x) = 1+x+pow(x,2)/fac(2)+pow(x,3)/fac(3)+pow(x,4)/fac(4)
...
Каждая функция немного точнее приближает ex, чем предыдущая. Здесь pow(x,n) — стандартная библиотечная функция, возвращающая xn. В стандартной библиотеке нет функции, вычисляющей факториал, поэтому мы должны определить ее самостоятельно.
int fac(int n) // factorial(n); n!
{
int r = 1;
while (n>1) {
r*=n;
––n;
}
return r;
}
Альтернативная реализация функции fac() описана в упр. 1. Имея функцию fac(), можем вычислить n-й член ряда.
double term(double x, int n) { return pow(x,n)/fac(n); } // n-й
// член ряда
Имея функцию term(), несложно вычислить экспоненты с точностью до n членов.
double expe(double x, int n) // сумма n членов для x
{
double sum = 0;
for (int i=0; i<n; ++i) sum+=term(x,i);
return sum;
}
Как построить график этой функции? С точки зрения программиста трудность заключается в том, что наш класс Function получает имя функции одного аргумента, а функция expe() имеет два аргумента. В языке С++ нет элегантного решения этой задачи, поэтому пока воспользуемся неэлегантным решением (тем не менее, см. упр. 3). Мы можем удалить точность n из списка аргументов и сделать ее переменной.
int expN_number_of_terms = 10;
double expN(double x)
{
return expe(x,expN_number_of_terms);
}
Теперь функция expN(x) вычисляет экспоненту с точностью, определенной значением переменной expN_number_of_terms. Воспользуемся этим для построения нескольких графиков. Сначала построим оси и нарисуем истинный график экспоненты, используя стандартную библиотечную функцию exp(), чтобы увидеть, насколько хорошо она приближается функцией expN().
Function real_exp(exp,r_min,r_max,orig,200,x_scale,y_scale);
real_exp.set_color(Color::blue);
Затем выполним цикл приближений, увеличивая количество членов ряда n.
for (int n = 0; n<50; ++n) {
ostringstream ss;
ss << " приближение exp; n==" << n ;
win.set_label(ss.str());
expN_number_of_terms = n;
// следующее приближение:
Function e(expN,r_min,r_max,orig,200,x_scale,y_scale);
win.attach(e);
win.wait_for_button();
win.detach(e);
}
Обратите внимание на последний вызов detach(e) в этом цикле. Область видимости объекта e класса Function ограничена телом цикла for. Каждый раз, кода мы входим в этот блок, мы создаем новый объект e класса Function, а каждый раз, когда выходим из блока, объект e уничтожается и затем заменяется новым. Объект класса Window не должен помнить о старом объекте e, потому что он будет уничтожен. Следовательно, вызов detach(e) гарантирует, что объект класса Window не попытается нарисовать разрушенный объект.
На первом этапе мы получаем окно, в котором нарисованы оси и “настоящая” экспонента (синий цвет).
Как видим, значение exp(0) равно 1, поэтому наш синий график “настоящей” экспоненты пересекает ось y в точке (0,1). Если присмотреться повнимательнее, то видно, что на самом деле мы нарисовали первое приближение (exp0(x)==0) черным цветом поверх оси x. Кнопка Next позволяет получить аппроксимацию, содержащую один член степенного ряда. Обратите внимание на то, что мы показываем количество сленгов ряда, использованного для приближения экспоненты, как часть метки окна.
Это функция exp1(x)==1, представляющая собой аппроксимацию экспоненты с помощью только одного члена степенного ряда. Она точно совпадает с экспонентой в точке (0,1), но мы можем построить более точную аппроксимацию.
Используя два члена разложения (1+x), получаем диагональ, пересекающую ось y в точке (0,1). С помощью трех членов разложения (1+x+pow(x,2)/fac(2)) можем обнаружить признаки сходимости.
Десять членов приближения дают очень хорошее приближение, особенно для значений x, превышающих –3.
На первый взгляд, мы могли бы получать все более точные аппроксимации, постоянно увеличивая количество членов степенного ряда. Однако существует предел, и после тринадцати членов происходит нечто странное: аппроксимация ухудшается, а после вычисления восемнадцати членов на рисунке появляются вертикальные линии.
Помните, что арифметика чисел с плавающей точкой — это не чистая математика. Числа с плавающей точкой просто хорошо приближают действительные числа, поскольку для их представления можно использовать лишь ограниченное количество бит. С определенного момента наши вычисления стали порождать числа, которые невозможно точно представить в виде переменных типа double, и наши результаты стали отклоняться от правильного ответа. Более подробная информация на эту тему приведена в главе 24.
Последний рисунок представляет собой хорошую иллюстрацию следующего принципа: если ответ выглядит хорошо, еще не значит, что программа работает правильно. Если программа проработает немного дольше или на несколько других данных, то может возникнуть