Главная страница
Навигация по странице:

  • Использование ссылок вместо передачи по значению

  • Размерность массива b известна на этапе компиляции, под массив а память выделяется динамически

  • // на функцию с одним параметром типа int

  • Указатели на функции передаются в подпрограмму таким же образом, как и параметры других типов

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

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

  • Рекурсия Информатика. Параметры функции


    Скачать 83.03 Kb.
    НазваниеПараметры функции
    АнкорРекурсия Информатика
    Дата01.12.2021
    Размер83.03 Kb.
    Формат файлаppt
    Имя файла1638357623540_1638357388648_IP_AiP_lek_2020_9_parametry_funktsiy.ppt
    ТипДокументы
    #288296

    Параметры функции

    Механизм параметров является основным способом обмена информацией между вызываемой и вызывающей функциями. Параметры, перечисленные в заголовке описания функции, называются формальными параметрами, или просто параметрами, а записанные в операторе вызова функции — фактическими параметрами, или аргументами. При вызове функции в первую очередь вычисляются выражения, стоящие на месте аргументов; затем в стеке выделяется память под формальные параметры функции в соответствии с их типом, и каждому из них присваивается значение соответствующего аргумента. При этом проверяется соответствие типов и при необходимости выполняются их преобразования. При несоответствии типов вы­ дается диагностическое сообщение. 1) При передаче по значению в стек заносятся копии значений аргументов (фактических параметров), и операторы функции работают с этими копиями. Доступа к исходным значениям параметров у функции нет, а, следовательно, нет и возможности их изменить. 2) При передаче по адресу в стек заносятся адреса аргументов, а функция осуществляет доступ к ячейкам памяти по этим адресам и может изменить исходные значения аргументов:

    #include

    void f( int i, int* j, int& k);

    int main(){

    int i = 1, j = 2, к = 3;

    cout << "i j k\n";

    cout<< i << ‘ ‘ << j << ‘ ‘ << k << '\n');

    f(i, &j, k);

    cout << i << ‘ ‘ << j << ‘ ‘ << k << '\n');

    return 0;}

    void f( int i, int* j, int& k) {

    i++; (*j)++; k++; }

    Результат работы программы:

    Результат работы программы:

    i j k

    1 2 3

    1 3 4

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

    Если требуется запретить изменение параметра внутри функции, используется модификатор const:

    int f(const char*);

    char* t(char* a, const int* b);

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

    #include

    #include

    int sum (const int* mas, const int n);

    int const n = 10;

    int main(){

    int marks[n] = {3, 4, 5, 4, 4};

    cout << "Сумма элементов массива: " << sum(marks, n);

    return 0; }

    int sum(const int* mas, const int n){

    // Bapианты: int sum(int mas[ ], int n)

    // или int sum(int mas[n], int n)

    // (величина n должна быть константой)

    int s = 0;

    for (int i = 0; i

    return s;}

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

    #include

    int sum(const int *a, const int nstr, const int nstb);

    int main(){

    int b[2][2] = {{2, 2}, {4, 3}};

    cout << “Cyммa элементов b: “, sum(&b[0][0], 2, 2);

    // имя массива передавать в sum нельзя из-за несоответствия типов

    int i, j, nstr, nstb, *a;

    cout << "Введите количество строк и столбцов: \п");

    cin >> nstr >> nstb;

    а = (int *)malloc(nstr * nstb * sizeof(int));

    for (i = 0; i

    for (j = 0; j> a[ i* nstb + j ];

    cout << “Cyммa элементов a: “, sum(a, nstr, nstb); return 0;

    }

    int i, j, s = 0,

    for (i = 0, i

    for (j = 0; j

    return s; }

    Для того чтобы работать с двумерным массивом естественным образом, можно применить альтернативный способ выделения памяти:

    #include

    #include

    int sum ( int **a, const int nstr, const int nstb);

    int main(){

    int nstr, nstb;

    cin >> nstr >> nstb;

    int **a, i, j;

    // Формирование матрицы a:

    a = new int* [nstr];

    for (i = 0; i

    a[i] = new int [nstb];

    for (i = 0; i

    for (j = 0; j < nstb; j++) cin >> a[i][j];

    cout << sum( a, nstr, nstb); }

    int sum( int **a, const int nstr, const int nstb){

    int i, j, s = 0;

    for (i = 0; i< nstr; i++)

    for (j=0; j< nstb; j++) s+= a[i][j];

    return s; }

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

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

    void f(int а ){/*...* / } // определение функции

    void (*pf)(int); // указатель на функцию

    pf = &f; // указателю присваивается адрес функции

    // (можно написать pf = f;)

    pf(10); // функция f вызывается через указатель pf

    // (можно написать (*pf)(10) )

    Для того чтобы сделать программу легко читаемой, при описании указателей на функции используют переименование типов (typedef). Можно объявлять массивы указателей на функции (это может быть полезно, например, при реализации меню);

    // на функцию с одним параметром типа int:

    // Описание и инициализация массива указателей:

    PF menu[ ] = {&new, &open, &save};

    menu[1](10); // Вызов функции open

    Здесь new, open и save — имена функций, которые должны быть объявлены ранее. Указатели на функции передаются в подпрограмму таким же образом, как и параметры других типов:

    #include

    void f1(PF pf){ // функция f1 получает в кач. параметра указатель типа PF

    pf(5); // вызов функции, переданной через указатель }

    void f( int i ){cout << i; }

    int main(){

    f1(f);

    return 0; }

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

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

    Эти параметры должны быть последними в списке и могут опускаться при вызове функции.

    Если при вызове параметр опущен, должны быть опущены и все параметры, стоящие за ним.

    В качестве значений параметров по умолчанию могут использоваться константы, глобальные переменные и выра­ жения:

    int f(int а, int b = 0);

    void f1(int, int = 100, char* = 0);

    /* обратите внимание на пробел между * и = (без него получилась бы операция сложного присваивания *=) */

    void err( int errValue = errno); // errno - глобальная переменная

    …..

    f(100); f(a, 1); // варианты вызова функции f

    f1(a); f1(a, 10); f1(a, 10, "Vasia"): // варианты вызова функции f1

    f1(a, ,"Vasia") // неверно!

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

    Проверка соответствия типов для этих параметров не выполняется, char и short передаются как int, а float — как double.

    В качестве примера можно привести функцию printf, прототип которой имеет вид:

    int printf (const char*, ...);

    Это означает, что вызов функции должен содержать по крайней мере один параметр типа char* и может либо содержать, либо не содержать другие параметры:

    printf("Введите исходные данные"); // один параметр

    printf("Сумма: %5.2f рублей", sum); // два параметра

    printf(“%d %d %d %d”, a, b, с, d); // пять параметров

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

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

    хотя можно представить случаи, когда переменное число параметров является лучшим решением

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

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

    Рекурсивной называется функция, которая вызывает саму себя. Такая рекурсия называется прямой. Существует еще косвенная рекурсия, когда две или более функций вызывают друг друга.

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

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

    Классическим примером рекурсивной функции является вычисление факториала (это не означает, что факториал следует вычислять именно так).

    Для того чтобы получить значение факториала числа n, требуется умножить на n факториал числа (n -1). Известно также, что 0!=1 и 1!=1.

    long fact(long n){ if (n==0 && n==1) return 1; return (n * fact(n - 1); }

    To же самое можно записать короче:

    long fact(long n){ return (n>1) ? n * fact(n - 1) : 1; }

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

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

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

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

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

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

    Компилятор определяет, какую именно функцию требуется вызвать, по типу фактических параметров. Этот процесс называется разрешением перегрузки (перевод английского слова resolution в смысле «уточнение»). Тип возвращаемого функцией значения в разрешении не участвует. Механизм разрешения основан на достаточно сложном наборе правил, смысл которых сводится к тому, чтобы использовать функцию с наиболее подходящими аргументами и выдать сообщение, если такой не найдется.

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

    int max(int, int):

    // Возвращает подстроку наибольшей длины:

    char* max(char*, char*);

    // Возвращает наибольшее, из первого параметра и длины второго:

    int max (int, char*);

    // Возвращает наибольшее из второго параметра и длины первого:

    int max (char*, int);

    void f(int a, int b, char* c, char* d)

    { cout << max (a, b) << max(c, d) << max(a, c) << max(c, b); }

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

    Далее выполняются стандартные преобразования типов, например, int в double или указателей в void*.

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

    Неоднозначность может появиться при:

    • преобразовании типа;

    • использовании параметров-ссылок;

    • использовании аргументов по умолчанию.

    #include

    float f(float i){

    cout << "function float f(float i)" << endl;

    return i; }

    double f(double i){

    cout << "function double f(double i)" << endl;

    return i*2; }

    int main(){

    float x = 10.09;

    double у = 10.09;

    cout << f(x) << endl; // Вызывается f(float)

    cout << f(y) << endl; // Вызывается f(double)

    /* cout << f(10) << endl; Неоднозначность - как преобразовать

    10: во float или double? */

    return 0; }

    Для устранения этой неоднозначности требуется явное приведение типа для константы 10.

    int f(int а, int b), а другая — как

    int f (int а, int &b), то компилятор не сможет узнать, какая из этих функций вызывается, так как нет синтаксических различий между вызовом функции, которая получает параметр по значению, и вызовом функции, которая получает параметр по ссылке ( f(c,d)).

    Пример неоднозначности при использовании аргументов по умолчанию: #include

    int f(int a){return a;}

    int f(int a, int b = 1){ return a * b;}

    int main(){

    cout << f(10, 2); // Вызывается f(int, int)

    /* cout << f(10); Неоднозначность - что вызывается: f(int, int) или f(int) ? */ return 0; }

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

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


    написать администратору сайта