|
Программирование >> Динамические структуры данных
функции указано, сколько величин и какого типа ей требуется для успешной работы, значит, надо их ей передать. Если мясоперерабатывающий конвейер рассчитан на то, что на вход поступает корова, а на выходе получается колбаса, трудно рассчитывать на колбасу, подав на вход даже самый современный автомобиль. Вызов функции, возвращающей значение определенного типа (то есть не имеющей тип void), может быть записан в любом месте, где по синтаксису допустимо выражение - в правой части оператора присваивания, в составе выражения, в цепочке вывода и так далее. Вот, например, как можно вызвать функции, приведенные выше: double у. х1 = 0.34. х2 = 2: у = sin(xl): cout у * sin(x2) endl: у = sin(xl + 0.5) - sin(xl - 0.5): char *cite = Never say never : char b[100]: memcpy(b. cite, strlen(cite) + 1); int summa. a = 2: summa = sum(a. 4): ВНИМАНИЕ-- В определении, в объявлении и при вызове одной и той же функции типы и порядок следования параметров должны совпадать. Для имен параметров никакого соответствия не требуется. Теперь, когда мы получили общее представление о назначении и правилах записи функций, перейдем к рассмотрению задач. Задача 7.1. Передача в функцию параметров стандартных типов Написать программу вывода таблицы значений функции Ch х (гиперболический косинус) для аргумента, изменяющегося в заданных пределах с заданным шагом. Значения функции вычислять с помощью разложения в ряд Тейлора с точностью е. На втором семинаре мы уже рассматривали подобные задачи (см. задачи 2.4 и 2.5), поэтюму принцип вычисления суммы ряда вам уже знаком. Алгоритм работы программы также приводился ранее: для каждого из серии значений аргумента вычисляется и затем выводится на экран значение функции. Очевидно, что подсчет суммы ряда для одного значения аргумента логично оформить в виде отдельной функции. ВНИМАНИЕ -- Разработка любой функции ведется в том же порядке, что и разработка программы в целом. Сначала определяется интерфейс функции, то есть какие значения подаются ей на вход и что должно получиться в результате. Затем продумываются структуры данных, в которых будут храниться эти и промежуточные значения; затем составляется алгоритм, программа и тестовые примеры. Нашей функции подсчета суммы ряда требуется получить извне значение аргумента и точность. Пусть эти величины, а также результат имеют тип doubl е. Следовательно, заголовок функции может выглядеть так: doubl е cosh(doublе х. doublе eps): Обратившись к задаче 2.5, мы видим, что для вычисления суммы ряда понадобятся две промежуточные переменные - для хранения очередного члена ряда и его номера. Эти переменные должны быть описаны внутри функции, поскольку вне ее они не нужны (вспомните, что еще на первом семинаре мы рекомендовали вам описывать переменные так, чтобы их область действия была минимальной из возможных). Давайте рассмотрим текст программы: #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( XlfXIfXIflKlf-.&Xn. &Xk. &dX. &eps): printf( --.......--.....---------------- \n ) printf( I X I Y \n ) pri ntf ( ------------.............-------\n ) for (double X = Xn: printf( !i;9.21f printf( --.....--- return 0: X <= Xk: X +- dX) (Ш.бд \n . X. cosh(x, eps)): ..........-----------\n ): double cosh(double x. double eps) { const int Maxlter = 500: 7/ максимальное количество итераций double ch - 1. у - ch: первый член ряда и нач. значение суммы for (int n - 0: fabs(ch) > eps: n++) { ch X * X /((2 * n + 1)*(2 * n + 2)): член ряда У ch: добавление члена ряда к сумме if (п > Maxlter) { puts( Pяд расходится!\п ): return 0: } return у: Как видите, за счет использования функции программа получилась более ясной и компактной, потому что задача была разделена на две: вычисление функции и печать таблицы. Кроме того, написанную нами функцию можно при необходимости без изменений перенести в другую программу или поместить в библиотеку. Если определение функции размещается после ее вызова, то перед функцией, в которой он выполняется, размещают прототип (заголовок). Обычно заголовки всех используемых в программе функций размещают в самом начале файла или в отдельном заголовочном файле*. Заголовок нужен для того, чтобы компилятор мог проверить правильность вызова функции. Стандартные заголовочные файлы, которые мы подключаем к программам, содержат прототипы функций библиотеки именно с этой целью. В этой программе для ввода-вывода мы применили не классы, а функции, унаследованные из библиотеки Языка С, поскольку с их помощью, на нащ взгляд, форматированный вывод записывается более компактно. Обратите внимание на спецификацию формата д. Она применяется для вывода вещественных чисел в широком диапазоне значений. Первое число модификатора (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: printfC Enter Xn. Xk. dX. eps \n ): seanf( XlfXIfXlfXlf-.&Xn. &Xk. &dX. &eps): printf( printf( I printfC - \n ) \n ) - \n ) В задаче 7.7 мы расскажем о том, как оформлять заголовочные файлы. for (double X - Xn: х <- Xk: х +- dX) { у = coshCx. eps. err): if (err) printf( Ji;9.21f Ряд расходится! \n , x): else printf( 1X9.21f X14.6g \n . x. y): pri ntf ( ................................\n ): return 0: double cosh(double x. double eps. int &err) { err - 0: const int Maxlter - 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 > Maxlter) { err = 1: return 0: } return y: Недостатком этого метода является увеличение количества параметров функции. Да и программа, использующая функцию, тоже несколько усложнилась! Надеемся, вы обратили внимание на знак & перед параметром err. Это - признак передачи параметра по ссылке. Такой способ позволяет передавать значения из функции в вызывающую программу. Сейчас самое время рассмотреть механизм передачи параметров в функцию. Он весьма прост. Когда мы пишем в списке параметров функции выражение вида doubl е X, это значит, что в функцию при ее вызове должно быть передано значение соответствующего аргумента. Для этого в стеке создается его копия, с которой и работает функция. Естественно, что изменение этой копии не может оказать никакого влияния на ячейку памяти, в которой хранится сам параметр. Кстати, именно поэтому на месте такого параметра можно при вызове задавать и выражение, например: у - cosh(x + 0.2. eps / 100. err): Выражение вычисляется, и его результат записывается в стек на место, выделенное для соответствующего параметра. Ссылка, синтаксически являясь синонимом имени некоторого объекта, в то же время содержит его адрес. Поэтому ссылку, в отличие от указателя, не требуется разадресовывать для получения значения объекта. Если мы передаем в функцию ссылку, то есть пишем в списке параметров выражение вида double &eps, а при вызове подставляем на его место аргумент, например eps f act, мы тем самым передаем в функцию адрес переменной eps f act. Этот адрес обрабатывается так же, как и остальные параметры: в стеке создается его копия. Функция, работая с копией адреса, имеет доступ к ячейке памяти, в которой хранится значение переменной epsf act, и тем самым может его изменить. Вот и все! Можно передать в функцию и указатель; в этом случае придется применять операции разадресации и взятия адреса явным образом. Для нашей функции применение указателя для передачи третьего параметра будет выглядеть так: прототип функции: double cosh(double х, double eps. int *егг): вызов функции: у = cosh(x, eps. &err): & - взятие адреса обращение к err внутри функции: *егг = 0: * - разадресация Как видите, в прототипе (и, конечно, в определении функции) явным образом указывается, что третьим параметром будет указатель на целое. При вызове на его место передается адрес переменной err. Чтобы внутри функции изменить значение этой переменной, применяется операция получения значения по адресу. Итак, мы видим, что для входных данных функции используется передача параметров по значению, для передачи результатов ее работы - возвращаемое значение и/или передача параметров по ссылке или указателю. На самом деле у передачи по значению есть один серьезный недостаток: для размещения в стеке копии данных большого размера (например, структур, состоящих из многих полей) тратится и время на копирование, и место. Кроме того, стек может просто переполниться. Поэтому более безопасный, эффективный и грамотный способ - передавать входные данные по ссылке, да не по простой, а по константной, чтобы исключить возможность непреднамеренного изменения параметра в функции. Для нашей программы передача входных данных по константной ссылке выглядит так: прототип функции: . double cosh(const double &x. const double &eps. int &err): вызов функции: у = cosh(x, eps. err): обращение к x и eps внутри функции не изменяется Поскольку вопрос о передаче параметров очень важен, не поленимся повторить изложенное еще раз в краткой форме: ВНИМАНИЕ - Входные данные функции иадр передавать по значению или 1ю константной ссылке, результаты ее работы - через возвращаемое значение, а при необходимости передачи более одной величины - через параметры по ссылке или указателю. Вернемся к обсуждению способов сообщения об ошибках внутри функции. Еще один способ - написать функцию так, чтобы параметр ошибки передавался через возвращаемое значение. Это применяется в основном для функций вывода информации. Например, функция стандартной библиотеки int fputc(int ch. FILE *f); записывает символ ch в поток f. При ошибке она возвращает значение EOF, иначе - записанный символ. В этом случае при необходимости передать, в точку вызова какие-либо другие результаты работы функции их передают через список параметров. Часто в функциях библиотеки в случае возникновения ошибки применяется и более простое решение: при ошибке возвращается значение, равное нулю, хотя ноль может и входить в множество допустимых значений результата. В этом случае у программиста нет средств отличить ошибочное значение от правильного. Например, таким образом реализованы уже известные вам функции atoi, atol и atof. При невозможности преобразовать строку в число соответствующего типа они возвращают ноль, и то же самое значение будет выдано в случае, если в строке содержался символ 0. Теперь, когда мы обсудили разные способы уведомления об ошибке в функции, подведем итоги: ВНИМАНИЕ -- При написании функции нужно предусмотреть все возможные ошибки и обеспечить гюльзо-вателя функции средствами их диагностики. Печать диапюстических сообщений внутри функции нежелательна. Во второй части практикума мы рассмотрим еще один- механизм уведомления о возникновении ошибки - генерацию исключения. А теперь давайте немножко упростим себе жизнь, воспользовавшись средством С++, называемым значениями параметров по умолчанию. Может оказаться неудобным каждый раз при вызове функции cosh задавать требуемую точность вычисления суммы ряда. Конечно, можно определить точность в виде константы внутри функции, задав максимальное допустимое значение, но иногда это может оказаться излишним, поэтому желательно сохранить возможность задания точности через параметры. Для этого либо в определении (если оно находится выше по тексту, чем любой вызов функции), либо в прототипе функции после имени параметра указывается его значение по умолчанию, например: double cosh(double х, double eps = DBL EPSILON):
|
© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки. |