
- •«Технология программирования»
- •«Функции», «Перегрузка и шаблоны функций»
- •Самара 2007
- •Лабораторная работа №7 функции
- •Краткие сведения из теории
- •Описание лабораторной установки и по
- •Указания к выполнению работы
- •Задание 3. Рекурсивные функции
- •Контрольные вопросы
- •Лабораторная работа №8 перегрузка и шаблоны функций
- •Краткие сведения из теории
- •Указания к выполнению работы
- •Контрольные вопросы
- •Приборы и программное обеспечение
- •Содержание отчета
- •Библиографический список
Описание лабораторной установки и по
Компьютер под управлением операционной системы 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 и возвращающей значение того же типа.
Как видите, наряду с большей общностью мы добились и лучшего структурирования программы, разбив ее на две логически не связанные подзадачи: вычисление функции и вывод таблицы. В главной программе остался только ввод исходных данных и вызов функции.