Аргументы функции

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

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

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

<тип> <имя> (<список явных параметров>, …)
{
<тело функции>
}

Здесь:
<тип> – тип возвращаемого значения,
<список параметров> – список параметров известных до вызова функции.

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

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

  • один из параметров определяет число параметров функции;
  • в списке явных параметров задается параметр, указывающий на конец списка параметров.

Если не один из этих способов не используется, тогда можно использовать специальный набор макроопределений.

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

Пример 1. Задание числа дополнительных параметров с помощью первого параметра. Функция вычисляет сумму значений дополнительных параметров. Список явных параметров состоит из одного параметра, который задает число дополнительных параметров.

#include <stdio.h>
#include <conio.h>
#include <iostream>
void main()
{ 
   int sum(int n,...); //прототип функции с переменным 
                       //числом параметров
   cout << "\n 4+6=" << sum(2,4, 6);
   cout << "\n 1+2+3+4+5+6=" << sum(6,1,2,3,4,5,6);
   cout << "\n Parametrov net.Summa ravna:" << sum(0);
   getch();
}
int sum(int n,...)//n-число суммируемых параметров
{
   int s=0;
   int *p = &n; //получение адреса параметров в стеке
   for(int i=1; i<=n; i++)
      s+=*++p; //суммируем числа
   return s;
}

Для доступа к параметрам, которые один за другим попали в стек, используется адрес первого параметра (*p = &n;), таким образом, указатель устанавливается на начало списка параметров в стеке (в памяти). Затем в цикле при помощи указателя p перемещаемся по параметрам и суммируем их, извлекая (*p) из памяти. Все параметры должны иметь одинаковый тип.

Проверка соответствия типов для дополнительных параметров не выполняется, поскольку компилятор не имеет информации, необходимой для проведения проверки. При вызове функции дополнительные параметры типа char и short передаются как int, а float — как double.

Пример 2. Определение конца списка параметров с помощью параметра индикатора. Функция product() вычисляет произведение дополнительных параметров. Как только встречается нулевой параметр, вычисление произведения прекращается. Указатель p указывает на начало списка параметров (double*p=&a;).

#include <iostream>
double product(double a, ...)
{
   double  b;
   if (a)//проверяем на отсутствие дополнительных параметров
   {  b = 1.0; //b - произведение
      //цикл до тех пор, пока p не укажет на 0
      for (double* p=&a; *p; p++)
      	  b *= *p;
   }
   else
      b = 0.0;
 
   return b;
}
 
void main()
{  double product(double a, ...);//прототип функции с
                                 // переменным числом параметров
   cout << "\n product (6e0, 5e0, 3e0, 0e0) = " 
        << product (6e0, 5e0, 3e0, 0e0);
   cout << "\n product (1.5f, 2.0f, 0.0f, 3.0f, 0.0f) = "
        << product (1.5f, 2.0f, 0.0f, 3.0f, 0.0f);
   cout << "\n product (0.0) = " << product (0.0);
   cout << "\n product (1.0e-28,0.0) = " 
        << product (1.0e-28,0.0);
 
   getch();
}

Напомним, что при вызове функции дополнительные параметры типа float передаются как double, так что для параметров типа float надо использовать функцию с параметрами типа double.

Для использования в программе функций с переменным числом параметров, при вызове которых можно использовать параметры разных типов, удобно использовать специальный набор макроопределений (заголовочный файл stdarg.h). Макрокоманды для доступа к списку фактических параметров переменной длины, имеют следующий формат:
//связывание переменной param с первым параметром
void va_start(va_list param,<последний явный параметр>);
//получение значения очередного параметра типа type
type va_arg(va_list param, type);
//организации корректного выхода из функции
void va_end(va_list param);

Кроме перечисленных макросов, в файле stdarg.h определен специальный тип данных va_list, который является указателем. Именно такого типа должны быть первые операнды, используемые при обращении к макрокомандам va_start, va_arg, va_end.

Определим порядок использования макросов. В теле функции обязательно определяется переменная типа va_list: va_list f. С помощью макроса va_start переменная f связывается с первым необязательным параметром, т.е. с началом списка неизвестной длины:

va_start(f,<последний явный параметр>)

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

va_arg(f, type);

позволяет получить значение очередного параметра типа type. Кроме того, макрос меняет значение указателя f на адрес следующего фактического параметра в списке.

Макрокоманда va_end предназначена для организации корректного выхода из функции с переменным числом параметров: va_end(f).

Приведем пример использования макросов в функции конкатенации любого числа строк. Строки передаются функции с помощью списка указателей. В конце списка помещается нулевой указатель NULL.

#include <stdio.h>
#include <conio.h>
#include <stdarg.h> //для макросов переменного списка параметров
#include <iostream>
 
using namespace std;
// функция конкатенации неопределенного числа строк
char *concat(char *s1, ...)
{
   va_list par;          //указатель на параметры списка
   char *cp;
   int len = strlen(s1); //длина 1-го параметра
   va_start(par,s1);     //начало переменного списка
 
   //цикл для определения общей длины параметров строк
   while(cp = va_arg(par, char *))
	     len += strlen(cp); 
 
   //выделение памяти для результата
   char *stroka = new char[len + 1];
   strcpy(stroka, s1);
 
   va_start(par, s1);//начало переменного списка
   //цикл конкатенации параметров строк
   while(cp = va_arg(par, char *))
       		strcat(stroka, cp);
 
   va_end(par);
   return stroka;
}
void main()
{
   setlocale(LC_CTYPE, "Russian");
   //прототип функции с переменным числом параметров
    char *concat(char *s1, ...); 
    char *s; //указатель для результата
    s = concat("\nVerba"," volant, ","scripta ","manent\n", 
    "(Слова исчезают, написанное остается.)", NULL);
    cout << s;
 
 _getch();
}

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

Следующий пример, функция miniprintf(), показывает, как это можно сделать. В этой функции допускаются только два вида форматов %d и %f, которые позволяют макросу va_arg() правильно извлекать значения. Окончание вывода определяется завершением перебора форматов.

#include <conio.h>
#include <stdarg.h> //для макросов переменного списка параметров
#include <iostream>
using namespace std;
 
void miniprintf(char *format, ...)
{
    va_list ap; //указатель на необязательный параметр
    char *p;	//для просмотра строки format
    int ii;	//целые параметры
    double dd;	//параметры типа double
 
    va_start(ap, format); //настроились на первый параметр
 
    for(p = format; *p; p++)
    {	if(*p != '%')
		{cout << *p;
		 continue;
		}
	switch (*++p)
	{	case 'd': ii = va_arg(ap, int);
			  cout << ii;
			  break;
		case 'f': dd = va_arg(ap, double);
			  cout << dd;
			  break;
		default:  cout << *p;
	}
    }//конец цикла просмотра строки форматов
 
    va_end(ap); //подготовка к завершению функции
}
 
void main()
{
     void miniprintf(char *format, ...);
     int k = 123;
     double d = 2.718282;
     miniprintf("\n Integer k = %d \n Double d = %f", k, d);
 _getch();	
}

 

Урок 9.7. Перегрузка функций

9.7. Перегрузка функций
9.7.1. Вызов функций при перегрузке
9.7.2. Перегрузка функций с несколькими аргументами
9.7.3. Перегрузка функций с аргументами по умолчанию
9.7.4. Перегрузка и область видимости

 

9.7. Перегрузка функций

Что такое перегрузка функций? В С++ несколько функций могут иметь одинаковые имена. Эта возможность используется, в основном, когда надо выполнить похожие действия, но над данными различных типов. Например:

 int min(int, int);
long min(long, long);
double min(double, double);

Вызов функции min(): double a = min(34.78, 88.77); Компилятор сам определит, какой конкретно экземпляр функции вызвать. Такой механизм называется перегрузкой функции.

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

Функции не считаются перегруженными если:

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

int min(int, int);
long min(int, int); //Error

Если сигнатуры различны, тогда функция считается перегруженной, при условии, что её экземпляры объявлены в одной зоне видимости.

Чаще всего механизм перегрузки используется при объявлении конструкторов класса и при перегрузке операций.

9.7.1. Вызов функций при перегрузке

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

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

Возможны три случая:

  1. Имеет место точное совпадение;

При этом char, short, 0 точно соответствуют типу int;

  1. Соответствие достигается при помощи преобразований;

При этом:

  • фактический аргумент любого числового типа может быть приведен к любому числовому типу;
  • константа 0 типа может быть преобразована к указателю;
  • любой указатель может быть преобразован к void *;
  • соответствие может быть достигнуто при помощи преобразований пользователя (в применении к фактическому параметру), т.е., если нет стандартных преобразований, тогда компилятор пытается применить преобразования определенные пользователем в классе.
  1. Соответствие достигается с более чем одним экземпляром функции, в этом случае выдается сообщение об ошибке. Например:

 

   void f(long);
   void f(int);
   void f(double);
   unsigned int ui;


Для вызова f(ui) есть 3 соответствия, каждое из которых достигается при помощи стандартного преобразования.

Пример. Применение комбинации преобразований – стандартных и определенных пользователем.

void f(void *);
void f(double);
class String{	// класс строк
     int sz;	//длина строки
     char *str;	//указатель на строку
   public://пользовательская перегруженная операция преобразования типа
    operator int(){ return sz;}
};
Рассмотрим следующие ситуации:
 
String s;
f(5); //стандартное преобразование int->double, выбирается f(double).
 
f(s); //1. преобразование пользователя в int, вызывается перегруженная 
      //   операция int
      //2. стандартное преобразование int->double,
      //3. выбирается f(double).
 
Два уровня пользовательских преобразований не допускается.
 
Примеры двусмысленных вызовов.
1. f(’a’); – ошибка, двусмысленная ситуация, точного соответствия нет, 
   а символ ’a’ может быть преобразован и к long и к double, 
   приоритетов преобразований нет. 
2. class String{ 
      Letter *str;
   public:
      // конструктор - преобразователь типа
      String(Letter); 
   };
 
   class Letter{ char ch;
        //преобразователь типа
       public: operator String();
   }
 
   void f(String);
   Letter L;
   f(L); //Ошибка двусмысленности, в обоих классах есть преобразование
         //типа, компилятор затрудняется выбрать одно преобразование
   f(String(L)); //Ошибка двусмысленности, неизвестно, что вызывать 
	         //конструктор String или перегруженный оператор String.
   f(L.operator String()); //разрешение двусмысленной ситуации: 
                           //явный вызов операции преобразования типа

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

9.7.2. Перегрузка функций с несколькими аргументами

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

   void f(long *, int);
   void f(int, int);
   f(5, ’c’);//соответствует void f(int, int);

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

   void f(int, double, long *);
   void f(int, long, void *);
   double a = 5, *pd;
   pd = &a;
   f(6, ’a’, pd);// соответствует void f(int, long, void *);

В этом примере по первому и второму аргументу ни одна функция не имеет преимущества, но так как double * нельзя привести к long *, а к типу void * можно, то второй экземпляр функции имеет преимущество и будет вызван.

9.7.3. Перегрузка функций с аргументами по умолчанию

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

   void f(int);
   void f(double, int = 0);
   f(5); 	//точно соответствует void f(int);
   f(5.7, 0); 	//точно соответствует void f(double, int = 0);
   f(7, 8); 	//соответствует void f(double, int ); 
	        //выбор по числу аргументов, 7 преобразуется в double
   f(34L);	// ошибка: двусмысленность по параметру и нельзя 
	        //сделать выбор по числу аргументов
   f(34.56);	//соответствует void f(double, int = 0);

9.7.4. Перегрузка и область видимости

Здесь действует два простых правила:

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

 

Урок 9.5. Модификаторы типа функции

Читать далее

Урок 9.6. Рекурсивные функции

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

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

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

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

Рассмотрим пример программы вычисляющей факториал положительного числа.

#include <stdio.h>
#include <conio.h>
#include <iostream>
using namespace std;
void main()
{
   int f(int k); //функция вычисления факториала
   int fact = f(5);
   cout << "fact! = " << fact;
getch();
}
// Рекурсивная функция вычисления факториала
int f(int k)
{
if (k < 0)  return 0;
if (k == 0) return 1; //т.к. 0! == 1
return k * f(k-1);
}

При первом вызове f(5) функция возвращает выражение 5*f(4), т.е. функция f() фактически не возвращает значение, а вызывает сама себя с другим значением. Рекурсивные вызовы будут продолжаться до тех пор, пока k не станет равным 0. Будет создана следующая цепочка возвращаемых выражений:

5*f(4), 4*f(3), 3*f(2), 2*f(1), 1*f(0)

Вызов f(0)не провоцирует дальнейших вызовов, а возвращает значение 1, произведение 1*1 будет возвращено предыдущему вызову и т.д. до вызова f(5), которому возвращается значение 120. Тем самым будут организованы следующие умножения:

1*1*2*3*4*5, а в общем случае 1*1*2*3*4*5*…*(k-1)*k

Рассмотрим еще один традиционный пример функции, представленной в форме прямой рекурсии, – печать чисел как последовательности символов.

#include <stdio.h>
#include <conio.h>
 
void main()
{ void printd(int n); 
  int n;
  n = 135;
  printd(n);
_getch();
}
//Рекурсивная функция 
void printd(int n) // печать n
{ int i;
  if (n < 0) 
  {
     putchar('-'); // печать минуса для отрицательных чисел
     n=-n;
  }
  if ((i = n / 10)!= 0) printd(i);
  putchar(n%10+'0'); // преобразование цифры в символ
}

Рассмотрим работу этой функции для числа n=153. Поскольку число положительно, то операторы печати знака минус putchar('-'); и смена знака числа n=-n; пропускаются.

Оператор if ((i=n/10)!=0) printd(i); проверяет целую часть частного 153/10 = 15 на ноль. Если целая часть не ноль, то с этим значением (15) идет вновь обращение к функции printd(), а после возврата из нее оператором putchar(n%10+'0'); печатается остаток от деления 153%10 = 3, т.е. 3. Аналогично, вторая printd() передает третьей 1 (та печатает ее), а затем сама печатает 5.

Таким образом, однократное обращение извне к функции printd(); вызовет трехкратное ее срабатывание.

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

Решение:

#include <stdio.h>
#include <conio.h>
#include <iostream>
 
using namespace std;
void main()
{  double power(double a, int n);//прототип функции
   double a = 5;
   int n = - 3;
 
   double d = power(a, n);
   cout << a << "^" << n << " = " << d; 
_getch(); 
} 
// Рекурсивная функция вычисления целой степени вещественного числа
double power(double a, int n)
{
   if (n == 0) return 1;
   if (a == 0) return 0;
   if (n > 0)  return a * power(a, n-1);
   if (n < 0)  return power(a, n+1) / a;
}

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

Решение (2 варианта summa() и summa1()):

#include <stdio.h>
#include <conio.h>
#include <iostream>
using namespace std;
void main()
{
   double summa (int *v, int n); //прототипы функций
   double summa1(int *v, int n);
   int v[5] = {1, 2, 3, 4, 5};
   double s = summa(v, 5);
   cout << "s = " << s << endl;
   double s1 = summa1(v, 5);
   cout << "s1 = " << s1 << endl; 
_getch(); } 
// Рекурсивные функции вычисления суммы элементов вектора 
double summa(int *v, int n) 
{    
   return n > 0 ? summa(v, n-1) + v[n-1] : 0;
}
double summa1(int *v, int n)
{
   return n > 0 ? summa(v+1, n-1) + *v : 0;
}

Рассмотрим пример косвенной рекурсии. Пример принадлежит к классу алгоритмов, работающих с данными, структура которых тоже определяется рекурсивно. Характерной задачей такого рода является преобразование простых выражений в соответствующую постфиксную форму (иначе Полиз – польскую инверсную запись), т.е. в форму, при которой знак операции следует за операндами. Выражение будем определять в виде РБНФ[*] так:

Выражение = Слагаемое{("+" I "-")Слагаемое}.
Слагаемое = Множитель{("*" I "/")Множитель}.
Множитель = Буква I "("Выражение")" I "["Выражение"]"

Обозначая выражение как Е, слагаемые как Т1, Т2, а множители как F1, F2, запишем правила преобразования:

T1+T2 --> T1T2+
T1-T2 --> T1T2-
F1*F2 --> F1F2*
F1/F2 --> F1F2/
(E) --> E
[E] --> E
___________________________________________________________________
[*]
РБНФ - расширенная форма Бэкуса-Наура, метанотация (формализм) для записи синтаксических конструкций. В данном примере использованы такие метасимволы:.,+,{,},(,),I. Эти метасимволы читаются следующим образом:

  • А = ВС - синтаксическая конструкция А состоит из следущих друг за другом конструкций В и С.
  • А = В I С - А состоит либо из В, либо из С.
  • А = {B} - А может состоять из конкатенации любого числа (включая ноль) конструкций В.

Наша программа отражает структуру синтаксиса входных выражений. Поскольку синтаксис рекурсивен, рекурсивна и сама программа.

#include <stdio.h>
#include <conio.h>
 
#define EOL 0x0a
char *ch = new char [100];
char *rez = new char [100];
char *rez1;
 
void Mnoj(); // множитель 
void Slag(); // слагаемое
void Vir();  // выражение
void main()
{
  rez1 = rez; //адрес rez будет меняться, а для вывода результата 
              //нужно начало строки
  scanf("%30s", ch); //не более 100 символов
  Vir();
  *rez = '\0'; 
  puts("rez1=");
  puts(rez1);
_getch();
}
 
void Vir() /* выражение */
{  char OpSl;
   Slag();
   while( (*ch == '+') || (*ch == '-')) 
       {
	 OpSl =* ch++; // операция сложения 
	 Slag();
	 *rez++ = OpSl;
       }
  //*rez = '\0';
} // конец выражения 
 
void Slag() /* слагаемое */
{  char OpUmn;
   Mnoj();
     while ((*ch == '*') || (*ch == '/')) 
	{
	   OpUmn =* ch++;
	   Mnoj();
	   *rez++ = OpUmn;
	}
}
 
void Mnoj() /* множитель */
{
   if (*ch == '(') 
	  { ch++; 
	    Vir();
	    while (*ch++ != ')'); 
	    return;
	   }
	else
	   if (*ch == '[') 
		  { ch++; 
		    Vir();
		    while (*ch != ']') ch++;
    	          }
	    else
		 while ((*ch < 'a') || (*ch > 'z'))
			ch++;
   *rez++ = *ch++;
} // конец множителя 
 
//Результат работы:
 >a+b          ab+
 >a*b+c        ab*c+
 >a+b*c        abc*+
 >a*(b/[c-d])  abcd-/*

 

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

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

 

Урок 9.4.6. Массивы в качестве аргументов

Читать далее

Translate Переводчик

Подписка на новости

SmartResponder.ru
Ваш e-mail: *
Ваше имя: *

Хостинг для Wordpress сайтов