Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
МУ7,8 (2011).doc
Скачиваний:
0
Добавлен:
01.05.2025
Размер:
207.87 Кб
Скачать

Описание лабораторной установки и по

Компьютер под управлением операционной системы Microsoft Windows 2000/XP, среда программирования Microsoft Visual Studio 6.0 или Borland C++ 3.1.

Указания к выполнению работы

Задание 1. Передача в функцию параметров стандартных типов

Написать программу вывода таблицы значений функции Ch x (гиперболический ко­синус) для аргумента, изменяющегося в заданных пределах с заданным шагом. Зна­чения функции вычислять с помощью разложения вряд Тейлора с точностью ε.

Для каждого из серии значений аргумента вы­числяется и затем выводится на экран значение функции. Очевидно, что подсчет суммы ряда для одного значения аргумента логично оформить в виде отдельной функции.

ВНИМАНИЕ: Разработка любой функции ведется в том же порядке, что и разработка программы в целом. Сначала определяется интерфейс функции, то есть, какие значения подаются ей на вход и что должно получиться в результате. Затем продумываются структуры данных, в которых будут храниться эти и промежуточные значения; затем составляется алгоритм, программа и тестовые примеры.

Нашей функции подсчета суммы ряда требуется получить извне значение аргу­мента и точность. Пусть эти величины, а также результат имеют тип double. Следо­вательно, заголовок функции может выглядеть так:

double cosh(double x, double eps);

Для вычисления суммы ряда понадобят­ся две промежуточные переменные для хранения очередного члена ряда и его номера. Эти переменные должны быть описаны внутри функции, поскольку вне ее они не нужны.

Рассмотрим текст программы:

#include <stdio.h>

#include <math.h>

double cosh(double x, double eps); // прототип функции

int main(){

double Xn, Xk, dX, eps;

printf("Enter Xn, Xk, dX, eps \n");

scanf("%lf%lf%lf%lf",&Xn, &Xk, &dX, &eps);

printf(" ------------------------------------------------- \n");

printf(“| X | Y |\n”); printf(“ ------------------------------------------------- \n");

for (double x = Xn; x <= Xk; x += dX)

printf("|%9.2lf |%14.6g |\n", x, cosh(x, eps));

printf(“ ------------------------------------------------- \n”);

return 0;

}

double cosh(double x, double eps){

const int MaxIter = 500; // максимальное количество итераций

double ch = 1, у = ch; // первый член ряда и нач. значение суммы

for (int n = 0; fabs(ch) > eps; n++){

ch *= x * x /((2 * n + 1)*(2 * n + 2)); // член ряда

у += ch; // добавление члена ряда к сумме

if (n > MaxIter){ puts("Ряд расходится!\n"); return 0; }

}

return у;

}

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

Если определение функции размещается после ее вызова, то перед функцией, в которой он выполняется, размещают прототип (заголовок). Обычно заголовки всех используемых в программе функций размещают в самом начале файла или в отдельном заголовочном файле. Заголовок нужен для того, чтобы компилятор мог проверить правильность вызова функции. Стандартные заголовочные файлы, ко­торые мы подключаем к программам, содержат прототипы функций библиотеки именно с этой целью.

В этой программе для ввода-вывода мы применили не классы, а функции, унасле­дованные из библиотеки языка С, поскольку с их помощью, на наш взгляд, форма­тированный вывод записывается более компактно. Обратите внимание на специ­фикацию формата g. Она применяется для вывода вещественных чисел в широком диапазоне значений. Первое число модификатора (14) задает, как и для других спецификаций, ширину отводимого под число поля, а второе (6) не точность, как в формате f, а количество значащих цифр. При этом число выводится либо в формате f, либо в формате е (с порядком) в зависимости от того, какой из них получится короче.

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

Во-первых, можно поступить так, как сделано в приведенной выше программе: вывести текстовое сообщение, сформировать какое-либо определенное значение функции (чаще всего это 0) и выйти из функции. Недостаток этого способа пе­чать диагностического сообщения внутри функции. Это нежелательно, а порой (например, когда функция входит в состав библиотеки) и вовсе недопустимо. По­пробуйте задать в качестве исходных данных большие значения аргумента и высо­кую точность. Вы увидите, что 500 итераций для ее достижения недостаточно, и таб­лицу результатов «портит» сообщение о том, что ряд расходится.

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

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

#include <stdio.h>

#include <math.h>

double cosh(double x, double eps, int &err);

int main(){

double Xn, Xk, dX, eps, y;

int err;

printf(“Enter Xn, Xk, dX, eps \n”);

scanf(“%lf%lf%lf%lf”,&Xn, &Xk, &dX, &eps);

printf(“ ------------------------------------------------- \n”);

printf(“| X | Y |\n");

printf(“ ------------------------------------------------- \n”);

for (double x = Xn; x <= Xk; x += dX) { у = cosh(x, eps, err);

if (err) printf("|%9.2lf | Ряд расходится! |\n", x);

else printf("|*9.2lf |%14.6g |\n", x, y);

}

printf(“ ------------------------------------------------- \n”);

return 0;

}

double cosh(double x, double eps, int &err) {

err = 0;

const int MaxIter = 500;

double ch = 1, у = ch;

for (int n = 0; fabs(ch) > eps; n++) {

ch *= x * x /((2 * n + 1)*(2 * n + 2));

у += ch;

if (n > MaxIter) { err = 1; return 0; }

}

return y;

}

Недостатком этого метода является увеличение количества параметров функции. Да и программа, использующая функцию, тоже несколько усложнилась! Надеем­ся, вы обратили внимание на знак & перед параметром err. Это признак переда­чи параметра по ссылке. Такой способ позволяет передавать значения из функции в вызывающую программу.

Сейчас самое время рассмотреть механизм передачи параметров в функцию. Он весьма прост. Когда мы пишем в списке параметров функции выражение вида double х, это значит, что в функцию при ее вызове должно быть передано значение соот­ветствующего аргумента. Для этого в стеке создается его копия, с которой и рабо­тает функция. Естественно, что изменение этой копии не может оказать никакого влияния на ячейку памяти, в которой хранится сам параметр. Кстати, именно по­этому на месте такого параметра можно при вызове задавать и выражение, напри­мер:

у = cosh(x + 0.2, eps / 100, err);

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

Ссылка, синтаксически являясь синонимом имени некоторого объекта, в то же время содержит его адрес. Поэтому ссылку, в отличие от указателя, не требуется разадресовывать для получения значения объекта. Если мы передаем в функцию ссылку, то есть пишем в списке параметров выражение вида double &eps, а при вы­зове подставляем на его место аргумент, например eps_fact, мы тем самым переда­ем в функцию адрес переменной eps_fact. Этот адрес обрабатывается так же, как и остальные параметры: в стеке создается его копия. Функция, работая с копией ад­реса, имеет доступ к ячейке памяти, в которой хранится значение переменной eps_fact, и тем самым может его изменить. Вот и все!

Можно передать в функцию и указатель; в этом случае придется применять опера­ции разадресации и взятия адреса явным образом. Для нашей функции примене­ние указателя для передачи третьего параметра будет выглядеть так:

// прототип функции:

double cosh(double x, double eps, int *err);

// вызов функции: у = cosh(x, eps, &err); // & - взятие адреса

// обращение к err внутри функции: *егг = 0; // * - разадресация

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

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

Для нашей программы передача входных данных по константной ссылке выгля­дит так:

// прототип функции:

double cosh(const double &x, const double &eps, int &err);

// вызов функции:

у = cosh(x, eps, err);

// обращение к х и eps внутри функции не изменяется

Поскольку вопрос о передаче параметров очень важен, не поленимся повторить изложенное еще раз в краткой форме:

ВНИМАНИЕ: Входные данные функции надо передавать по значению или по константной ссылке, ре­зультаты ее работы — через возвращаемое значение, а при необходимости передачи более одной величины через параметры по ссылке или указателю.

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

int fputc(int ch, FILE *f);

записывает символ ch в поток f. При ошибке она возвращает значение EOF, иначе записанный символ. В этом случае при необходимости передать в точку вызова какие-либо другие результаты работы функции их передают через список пара­метров.

Часто в функциях библиотеки в случае возникновения ошибки применяется и более простое решение: при ошибке возвращается значение, равное нулю, хотя ноль может и входить в множество допустимых значений результата. В этом случае у программиста нет средств отличить ошибочное значение от правильного. Напри­мер, таким образом, реализованы уже известные вам функции atoi, atol и atof. При невозможности преобразовать строку в число соответствующего типа они возвра­щают ноль, и то же самое значение будет выдано в случае, если в строке содержал­ся символ 0.

Теперь, когда мы обсудили разные способы уведомления об ошибке в функции, подведем итоги:

ВНИМАНИЕ: При написании функции нужно предусмотреть все возможные ошибки и обеспечить пользо­вателя функции средствами их диагностики. Печать диагностических сообщений внутри функции нежелательна.

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

А теперь давайте немножко упростим себе жизнь, воспользовавшись средством C++, называемым значениями параметров по умолчанию. Может оказаться неудоб­ным каждый раз при вызове функции cosh задавать требуемую точность вычисле­ния суммы ряда. Конечно, можно определить точность в виде константы внутри функции, задав максимальное допустимое значение, но иногда это может оказать­ся излишним, поэтому желательно сохранить возможность задания точности че­рез параметры. Для этого либо в определении (если оно находится выше по тексту, чем любой вызов функции), либо в прототипе функции после имени параметра указывается его значение по умолчанию, например:

double cosh(double x, double eps = DBL_EPSILON);

Нашу функцию можно вызывать с одним параметром, к примеру:

у = cosh(x);

ВНИМАНИЕ: Функция может иметь несколько параметров со значениями по умолчанию. Они должны находиться в конце списка параметров.

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

double cosh(const double x, int &егг, const double eps = DBL_EPSILON);

Соответствующим образом изменится и вызов функции. Обратите внимание, что указание перед параметром ключевого слова const в данном случае (при передаче по значению) применяется только для того, чтобы четко указать, какие из пара­метров являются входными. В случае передачи по ссылке указание const, кроме того, дает возможность передавать на месте этого параметра константу.

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

Задание 2. Передача в функцию имени функции

Назовем функцию вывода таблицы значений print_tabl. Прежде всего надо опре­делить ее интерфейс. Для того чтобы вывести таблицу, нашей функции потребует­ся знать диапазон и шаг изменения значений аргумента, а также какую, собственно, функцию мы собираемся вычислять. Всё? Нет, не все: забыли, что в функцию вычисления суммы ряда надо передавать точность, поэтому точность следует включить в список параметров вызывающей ее функции print_tabl. Функция рrint_tabl не возвращает никакого значения, то есть перед ее именем надо указать void.

Как передать в функцию имя функции? Точно так же, как и любую другую вели­чину: в списке параметров перед именем параметра указать его тип. До этого момента мы передавали в функцию величины стандартных типов, а теперь нам потребуется определить собственный тип. Тип функции определяется типом ее возвращаемого значения и типом ее параметров. Для нашей функции это выгля­дит так:

double (*fun)(double, double);

Здесь описывается указатель по имени fun на функцию, получающую два аргу­мента типа double и возвращающую значение того же типа (от параметров по умолчанию нам, к сожалению, придется отказаться). Часто, если описание типа сложное, с целью улучшения читаемости программы задают для него синоним с помощью ключевого слова typedef:

typedef double (*Pfun)(double, double);

В этом операторе задается тип Pfun, который можно использовать наряду со стан­дартными типами при описании переменных. Таким образом, заголовок функции печати таблицы должен иметь вид:

void print_tabl(Pfun fun, double Xn, double Xk, double dX, double eps);

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

#include <stdio.h>

#include <math.h>

typedef double (*Pfun)(const double, const double);

void print_tabl(Pfun fun, const double Xn, const double Xk, const double dX,

const double eps);

double cosh(const double x, const double eps);

int main() {

double Xn, Xk, dX, eps;

printf("Enter Xn, Xk, dX, eps \n");

scanf(“%lf,%lf%lf%lf”,&Xn, &Xk, &dX, &eps);

print_tabl(cosh, Xn, Xk, dX, eps);

return 0;

}

void print_tabl(Pfun fun, const double Xn, const double Xk, const double dX, const

double eps) {

printf(“--------------------------------------- \n");

printf(“l X | Y |\n”);

printf(“ -------------------------------------- \n”);

for (double x = Xn; x <= Xk; x += dX)

printf("|%9.2lf |%14.6g |\n", x, fun(x, eps)); printf(“ -------------------------------------- \n”);

}

double cosh(const double x, const double eps) {

const int MaxIter = 500;

double ch = 1, у = ch;

for (int n = 0; fabs(ch) > eps; n++) {

ch *= x * x 1(2 * n + 1)1(2 * n + 2);

у += ch;

If (n > Maxlter) return 0;

}

return y;

}

Функция print_tabl предназначена для вывода таблицы значений любой функ­ции, принимающей два аргумента типа double и возвращающей значение того же типа.

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