Нижегородский государственный университет им. Н.И.Лобачевского
Опубликован: 25.11.2008 | Доступ: свободный | Студентов: 9592 / 1296 | Оценка: 4.06 / 3.66 | Длительность: 21:16:00
Лекция 8:

Функции и их аргументы

< Лекция 7 || Лекция 8: 123 || Лекция 9 >

7.9. Рекурсивные функции

Рекурсивные определения и рекуррентные вычислительные схемы довольно часто используются в математике. Например:

n! = n*(n-1)!		//рекурсивное определение факториала
yn+1=0.5*(yn+x/yn)	//итерационная формула метода Ньютона

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

Наиболее часто цитируемым примером рекурсивной программы является вычисление факториала:

long fact(int n)
{ if (n<2) return 1;
  return (n*fact(n-1));
}

Еще один пример, демонстрирующий вычисление n-го элемента в последовательности чисел Фибоначчи: Fn=Fn-2+Fn-1

int Fib(int n)
{ if(n<3) return 1;
  return Fib(n-2)+Fib(n-1);
}

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

long fact(int n)
{ long f=1;
  for(int j=2; j<=n; j++) f=f*j;
  return f;
}
//-------------------------------
int Fib(int n)
{ int j,f,f1=1,f2=1;
  if(n<3) return 1;
  for(j=3; j<=n; j++)
    { f=f1+f2; f1=f2; f2=f; }
  return f;
}

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

  1. НОД(n1,n2)=НОД(n2,n1)
  2. НОД(0,n2)=n2
  3. НОД(n1,n2)=НОД(n2,n3), где n3=n1(mod n2)
//Рекурсивный вариант нахождения НОД
int nod(int n1,int n2)
{ if(n1==0) return n2;
  return nod(n2%n1,n1);
}

Эту функцию тоже несложно преобразовать в программу без рекурсии:

int nod(int n1,int n2)
{ int t;
m:  if(n2<n1) {t=n1; n1=n2; n2=t; }
  if(n1==0) return n2;
  n2=n1%n2;
  goto m;
}

7.10. Указатели на функцию и передача их в качестве параметров

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

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

Объявление указателя pf на функцию f(x), аргумент которой и возвращаемое значение имеют тип double, выглядит следующим образом:

double (*pf)(double x);

Оно напоминает прототип функции, в котором имя функции заменено именем указателя, заключенным в круглые скобки.

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

double int_rect(double a, double b, double (*f)(double x))
{ int i, n=100;
  double s=0,h=(b-a)/n;
  for(i=0; i<=n; i++) s += f(a+i*h);
  return s*h;
}

Последним аргументом процедуры int_rect является указатель на функцию. Поэтому в качестве фактического параметра мы должны подставить имя подынтегральной функции. Таковым, в частности, может быть имя любой стандартной функции из библиотеки math.h:

cout << int_rect(0,M_PI,sin) << endl;	//результат= 1.99984
cout << int_rect(0,M_PI,cos) << endl;	//результат=-4.18544e-17

В качестве второго примера рассмотрим программу нахождения корня уравнения y=f(x), если известно, что на интервале [ x1, x2 ] эта функция меняет знак. Алгоритм базируется на делении отрезка пополам. В точке xmid=(x1+x2)/2 смотрим знак функции f, который совпадет либо со знаком f(x1), либо со знаком f(x2). Выбираем ту половину отрезка, на концах которой функция принимает разные знаки. Затем исследуем его середину и т.д. Как только длина очередного отрезка станет достаточно малой или значение функции в центре отрезка окажется меньше заданной точности, процесс поиска корня прекращается.

#include <iostream.h>
#include <conio.h>
#include <math.h>
double y(double x)	//функция f(x)=x2-4
{ return x*x-4; }
double root(double x1,double x2,double eps,double(*f)(double x))
{ double f12,f1,f2,xmid;
  f1=f(x1);  f2=f(x2);
  if(f1*f2>0)
    { cerr<<"Error: sign(f1)=sign(f2)"; getch(); exit(0); }
  while(x2-x1 > eps)
  { xmid=(x1+x2)/2.;
    f12=f(xmid);
    if(fabs(f12) < eps)
      return xmid;
    if(f12*f1>0) { x1=xmid; f1=f12; }
    else {x2=xmid; f2=f12; }
  }
  return (x1+x2)/2.;
}void main()
{  cout<<root(0,10,1e-4,y);
  getch();
}
//=== Результат работы ===
2.00001

7.11. "Левые" функции

В документации по системам программирования и в сообщениях об ошибках иногда можно встретить термины lvalue и rvalue. Они обозначают, соответственно, величины, которые могут находиться слева ( lvalue = left value ) от знака равенства в операторе присваивания или справа ( rvalue = right value ).

Как правило, функции, возвращающие значение, используются в правой части оператора присваивания. Однако функции в качестве своего значения могут возвращать указатели и ссылки. А по указателям и ссылкам возможна запись. Именно такие функции называют "левыми".

Приведем в качестве примера функцию, возвращающую ссылку на максимум из двух своих аргументов:

double& max(double &x, double &y)
{ return (x>y)? x : y; }

Ее обычное использование:

double r=max(a,b);

Использование с учетом "левизны":

double a=5,b=6;
max(a,b)=10;	//эквивалентно b=10;

Аналогичный вариант, когда функция max возвращает указатель:

double* max(double *x, double *y)
{ return (*x>*y)?*x:*y; }
.........................
  double a=5,b=6;
  *max(&a,&b)=10;	//эквивалентно b=10;

Левая функция, возвращающая ссылку на максимальный элемент массива:

int& Mmax(int a[],int n)
{ int im=0;	//индекс максимального элемента
  for(int j=1;j<n;j++) im=(a[im]>a[j])? im : j;
  return a[im];
}

Левая функция, возвращающая указатель на максимальный элемент массива:

int* Mmax(int a[],int n)
{ int im=0;	//индекс максимального элемента
  for(int j=1;j<n;j++) im=(a[im]>a[j])? im : j;
  return &a[im];
}

Для запрета левого использования функции, возвращающей указатель или ссылку, достаточно разместить в начале ее заголовка спецификатор const:

const double& max(double &x, double &y)
{ return (x>y)? x : y); }
< Лекция 7 || Лекция 8: 123 || Лекция 9 >
Alexey Ku
Alexey Ku

Попробуйте часть кода до слова main заменить на 

#include "stdafx.h" //1

#include <iostream> //2
#include <conio.h>

using namespace std; //3

Александр Талеев
Александр Талеев

#include <iostream.h>
#include <conio.h>
int main(void)
{
int a,b,max;
cout << "a=5";
cin >> a;
cout <<"b=3";
cin >> b;
if(a>b) max=a;
else max=b;
cout <<" max="<<max;
getch();
return 0;
}

при запуске в visual express выдает ошибки 

Ошибка    1    error C1083: Не удается открыть файл включение: iostream.h: No such file or directory    c:\users\саня\documents\visual studio 2012\projects\проект3\проект3\исходный код.cpp    1    1    Проект3

    2    IntelliSense: не удается открыть источник файл "iostream.h"    c:\Users\Саня\Documents\Visual Studio 2012\Projects\Проект3\Проект3\Исходный код.cpp    1    1    Проект3

    3    IntelliSense: идентификатор "cout" не определен    c:\Users\Саня\Documents\Visual Studio 2012\Projects\Проект3\Проект3\Исходный код.cpp    6    1    Проект3

    4    IntelliSense: идентификатор "cin" не определен    c:\Users\Саня\Documents\Visual Studio 2012\Projects\Проект3\Проект3\Исходный код.cpp    7    1    Проект3

при создании файла я выбрал пустой проект. Может нужно было выбрать консольное приложение?