Введение в языки программирования C и C++

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

Показывать лекцию целиком

Функции являются основными программными единицами в языках C, C++. Из них как из кирпичиков складывается программа. В отличие от алголоподобных языков в программах на C, C++ не допускается вложенность функций. Любая программа на C должна содержать главную функцию с именем main, с которой начинается выполнение программы. Вызов всех остальных функций прямо или косвенно инициируется главной функцией.

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

В системах программирования на IBM-совместимых компьютерах используются два основных механизма передачи параметров – через стек и через машинные регистры. Первый способ наиболее распространен, т.к. он не ограничивает объем передаваемой информации. В качестве стека используется определенный участок оперативной памяти с фиксированным, но управляемым диапазоном адресов (размер стека можно регулировать при настройке компилятора). Специальный регистр "следит" за очередным доступным участком стека. По адресу, хранящемуся в этом регистре, можно положить нужную порцию данных в стек и одновременно продвинуть содержимое регистра стека. Для этой цели система машинных команд предусматривает специальную операцию PUSH (от англ. – протолкнуть). Вторая машинная операция POP (от англ. pop up – выскочить наверх) позволяет извлечь из стека очередную порцию данных с одновременной коррекцией регистра стека. Системная программа, обслуживающая стек, следит за тем, чтобы стек не переполнился при записи и не оказался пустым при извлечении данных. Иногда механизм работы стека сравнивают с магазином огнестрельного оружия – в нем, пуля, попавшая последней в рожок автомата, стреляет первой. Этим же объясняется технология обслуживания стека LIFO (Last In – First Out, т.е. последним вошел – первым вышел). Зарядка магазина имитирует запись в стек, а процедура стрельбы напоминает извлечение данных из стека.

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

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

int main(int argc, char* argv[])

или

int main(int argc, char **argv)

Предположим, что наша программа с именем nameprog.exe была запущена из командной строки со следующими аргументами:

>nameprog par1 par2 par3

Тогда первый аргумент главной функции argc будет равен 4 (имя программы входит в список параметров командной строки). Второй аргумент функции main представляет собой строковый массив, элементами которого являются отдельные компоненты командной строки:

argv[0] argv[1] argv[2] argv[3]
"nameprog" "par1" "par2" "par3"

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

>pkzip –a –r arch.zip qq1.doc qq2.doc

Функция может возвращать значение, – результат своей работы, или выполнять некоторое другое действие, не связанное с возвратом результата. Например, функция clrscr() осуществляет очистку экрана, но не возвращает никакого значения. Если функция возвращает значение, то в ее заголовке перед именем функции должен быть указан тип возвращаемого значения:

double mid(double x,double y)

В теле функции, возвращающей значение, обязан присутствовать оператор return (от англ. – возврат), содержащий результат работы функции – ее значение:

double mid(double x, double y)
{ return (x+y)/2.; }

Если функция не возвращает значение, то в ее заголовке перед именем функции должен быть указан тип void. В этом случае в теле функции может встретиться оператор return без параметра. Но оператор return может и отсутствовать – выход из функции произойдет при достижении последней фигурной скобки:

void print_v(int *a,int n)
{ int j;
  printf("\n");
  for(j=0; j<n; j++)
    printf("%8d",a[j]);
  printf("\n");
}

Если перед именем функции не указан ни один из стандартных типов и отсутствует спецификатор void, то считается, что функция возвращает значение типа int.

7.1. Параметры-значения

Формальный параметр в заголовке функции называют параметром-значением, если перед его именем указан только тип. Например, функция mid, вычисляющая среднее арифметическое двух величин, получает в качестве фактических аргументов два числовых значения определенного типа:

double mid(double x, double y)
{ return (x+y)/2.; }

В качестве фактических аргументов, соответствующих параметрам- значениям, могут быть заданы любые числовые выражения (формулы):

w1 = mid(x*cos(fi)+y*sin(fi), x*sin(fi)-y*cos(fi));

Значения этих выражений вычисляются и записываются в стек, откуда их извлекает функция mid и помещает переданные значения в свои локальные переменные x и y (формальные параметры-значения можно рассматривать как локальные переменные функции). При необходимости, значение вычисленного выражения автоматически приводится к типу формального параметра. После работы функции возвращаемый результат возвращается в специально выделенном регистре.

Почти все математические функции раздела math.h используют передачу аргумента по значению.

7.2. Параметры-указатели

Формальный параметр в заголовке функции называют явным параметром- указателем, если перед его именем находится символ *. Например, функция swap1, осуществляющая перестановку местами значений двух переменных, должна получить в качестве параметров адреса этих переменных, т.е. указатели на них:

void swap1(int *x,int *y)	//явные параметры-указатели
{ int tmp=*x;
  *x=*y;  *y=tmp;
}

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

#define pint int*
...............
void swap1(pint x,pint y)       //косвенные параметры указатели
{ int tmp=*x;
  *x=*y;  *y=tmp;
}

Косвенное объявление указателей может использовать и другую синтаксическую конструкцию:

typedef int* pint;

Представим себе, что функция swap1 была бы оформлена с параметрами-значениями:

void swap1(int x,int y)
{ int tmp=x;
  x=y;  y=tmp;
}

Тогда переставляемые значения поступили бы через стек и попали бы в локальные переменные (формальные параметры) x и y. Перестановка значений в локальных переменных была бы произведена, но вызывающая программа об этом ничего бы не узнала.

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

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

int x=2,y=3;
  swap1(x,y);

Рассмотрим еще один пример передачи указателя по указателю:

void swap3(int **v1, int **v2)
{ int *tmp=*v1; *v1=*v2; *v2=tmp; }
К этой функции можно обратиться следующим образом:
int i=10, j=20;
int *pi=i, *pj=j;
swap(pi,pj);

после такого обращения указатели pi и pj "смотрят" на новые значения, т.к. они поменялись адресами ( *pi=20, *pj=10 ), но сами переменные i и j свои значения не поменяли.

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

int sum_v(int *a,int n)
{ int j,s=0;
  for(j=0; j<n; j++)
    s += a[j];
  return s;
}

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

int q[20];
..........
  k1=sum_v(q,20);       //суммирование всех компонент вектора
  k2=sum_v(q,10);       //суммирование первых 10 компонент вектора
  k3=sum_v(q[5],3);    //суммирование q[5]+q[6]+q[7]
  k4=sum_v(q+5,3);      //суммирование q[5]+q[6]+q[7]

Не забывайте, что имя массива одновременно является и указателем на его первый элемент (т.е. q и q[0] – это одно и то же).

7.3. Параметры-ссылки

Формальный параметр в заголовке функции называют явным параметром-ссылкой, если перед его именем находится символ . Например, функция swap2, осуществляющая перестановку местами значений двух переменных, должна получить в качестве параметров ссылки на эти переменные, т.е. их адреса:

void swap2(int x,int y)    //явные параметры-ссылки
{ int tmp=x;
  x=y;  y=tmp;
}

Параметры-ссылки могут быть объявлены в заголовках функций и с помощью косвенных типов, предварительно описанных в конструкциях #define или typedef:

#define rint int
//или
  typedef int rint;
...................
void swap2(rint x,rint y)

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

int x=2,y=3;
  swap2(x,y);

По ссылке можно передавать не только имена переменных, но и имена указателей:

void swap3(int *v1,int *v2)
{ int *t=v2; v2=v1; v1=t; }

К этой функции можно обратиться следующим образом:

int i=10, j=20;
int *pi=i, *pj=j;
swap3(pi,pj);

После этого обращения указатели pi и pj "смотрят" на новые значения ( *pi=20, *pj=10 ), но сами переменные i и j сохранили свои прежние значения.

7.4. Параметры-константы

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

Оказывается, в этом скрывается некоторая уловка – если параметр объявлен как константная ссылка, то в качестве фактического аргумента в этом случае можно задавать не только имя переменной, но и выражение. Таким образом, параметр-ссылка уподобляется параметру-значению. Правда, при этом значение фактического аргумента не попадает в стек. Оно вычисляется и помещается в некоторую скрытую переменную в теле вызывающей функции. А вызываемой функции сообщается адрес этой скрытой переменной, что не меняет логику работы со ссылками. Но запись по этому адресу будет блокирована.

7.5. Параметры по умолчанию

В языке C++ допускается определение функций, у которых в заголовке указаны значения некоторых параметров:

double mid1(double x=0.5, double y=0.5)
  { return (x+y)/2.; }

К такой функции можно обратиться с одним (первым) аргументом или вообще без аргументов:

z=mid1(0.75);	//результат равен 0.625=(0.75+0.5)/2.
  p=mid1();	//результат равен 0.5=(0.5+0.5)/2.

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

double mid2(double x=0.5,double y)

В отличие от этого следующий заголовок считается правильным:

double mid3(double x,double y=0.5)

Обратите внимание на одну практическую деталь. Функция с параметрами по умолчанию работает правильно в двух случаях. Во-первых, если ее описание находится выше вызывающей функции и в заголовке функции содержится информация о параметрах по умолчанию. Во-вторых, если ее описание находится ниже и в заголовке функции отсутствуют сведения о параметрах по умолчанию, но они содержатся в прототипе. Одновременное упоминание значений по умолчанию и в заголовке функции, и в прототипе приводит к сообщению об ошибке как в системе BC 3.1, так и в ВСВ. Ниже приводится один из вариантов правильного оформления такой программы:

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

double mid(double x=1.,double y=1.);

void main()
{ //double mid(double x,double y);
  double x=0.4,y=0.2,z;
  z=mid(x,y); cout<<"z="<<z<<endl;
  z=mid(x);   cout<<"z="<<z<<endl;
  z=mid();    cout<<"z="<<z<<endl;
  getch();
}

double mid(double x,double y)
{ return (x+y)/2.; }

7.6. Функции с переменным количеством аргументов

Постоянная работа с функциями типа printf или scanf вызывает у программистов зависть – это же функции с переменным количеством аргументов. А как написать свою функцию, обрабатывающую столько параметров, сколько будет задано в обращении, и, естественно, допускающую задание разного количества аргументов?

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

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

double mid_var(int n,...)

Три точки в конце списка предупреждают компилятор о том, что он не должен контролировать количество и типы следующих аргументов. Все заботы о доступе к списку параметров переменной длины берет на себя вызываемая функция. Предположим, далее, что все аргументы передаются в функцию mid_var как значения, т.е. к моменту передачи управления функции они находятся в стеке. Добраться до них можно следующим образом. Заведем указатель типа int и занесем в него адрес формального параметра n (система знает, где находится стек, и адрес n ей доступен):

int *ip=n;

Продвинем указатель ip на 1, т.е. переместимся на адрес начала следующего параметра, и занесем его в новый указатель dp уже типа double:

ip++;	//переход на адрес первого слагаемого
  double *dp=(double *)ip; 	//преобразование типа указателя

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

double mid_var(int n,...)
{ int *ip=n+1;
  double *dp=(double *)ip; 
  double s=0.;
  for(int j=0; j<n; j++)
    s += dp[j];	//или s += *(dp+j); или s += *(dp++);
  return s/n;
}

Теперь попытаемся построить аналогичную функцию, которая суммирует свои аргументы до тех пор, пока не встречает нулевое слагаемое. Она устроена еще проще:

double mid_var(double a1,...)
{ double *dp=a1;
  double s=0;
  int c=0;
  while(*dp != 0)
    { s += *(dp++); c++; }
  return s/c;
}

Аналогичные функции можно построить, когда список передаваемых параметров состоит из переменного количества однотипных указателей. Только здесь придется использовать не просто указатели типа *dp, а "двойные" указатели типа **dp. И доставать значения нужных данных придется также через двойные указатели s += (**dp);

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

va_list p;		//объявление указателя на список параметров
va_start(p,p1);	//установка указателя списка на последний явный
			//параметр
va_arg(p,тип);	//перемещение указателя на очередной неявный параметр
va_end(p);		//уничтожение указателя на список параметров

Продемонстрируем использование этих средств на примере той же самой функции mid_var:

double mid_var(int n,...)	//функции передают количество параметров
{ va_list p;
  double s=0,c=0;
  va_start(p,n);
  while(n--)	//до тех пор, пока n != 0
  { s += va_arg(p,double); c++; }
  va_end(p);
  return s/c;
}

Если список параметров начинается с первого слагаемого a1, то программа меняется очень незначительно:

double mid_var(double a1,...)
{ va_list p;
  double s=0,c=0,u=a1;
  va_start(p,a1);
  do {s += u; c++; }
  while(u=va_arg(p,double));	//до тех пор, пока u != 0
  va_end(p);
  return s/c;
}

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

7.7. Локальные, глобальные и статические переменные

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

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

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

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

  • глобальная переменная объявлена без спецификатора static ;
  • в другом программном файле или в теле "чужой" функции эта переменная упомянута со спецификатором extern.
  • Глобальная переменная, объявленная со спецификатором static, доступна только для функций данного программного файла.

    7.8. Возврат значения функции

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

    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;
    }

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

  • НОД(n1,n2)=НОД(n2,n1)
  • НОД(0,n2)=n2
  • НОД(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); }
    Вернуться к учебному плану