Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лабы / Lab6 / ЯП_лаб_6.doc
Скачиваний:
3
Добавлен:
18.02.2023
Размер:
357.89 Кб
Скачать

Лабораторная работа № 6

Тема: Программирование с использованием функций

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

Краткая теория

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

В языке «С» различают два вида функций: библиотечные и определенные пользователем. К библиотечным функциям относят те функции, которые определены в стандартных библиотеках языка «С», например printf, scanf, gets, sin, cos и т.д. К определенным пользователем функциям относят функции, реализованные непосредственно в программе. Например, функция main.

Принцип подхода «сверху - вниз», реализованный в языке «С», позволяет разбивать разрабатываемую программу на отдельные блоки и оформлять их в виде отдельных функций. Разработанные пользователем функции вызываются из других пользовательских функций или непосредственно из функции main.

Описание функции на языке «С» состоит, как правило, из трех частей:

  1. Прототип функции – необязательная часть, содержит простое описание функции.

  2. Заголовок функции – содержит описание функции.

  3. Тело функции – содержит программный код функции.

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

тип_значения имя_функции(список типов формальных параметров);

Примеры:

double factorial(int);

int NumberOfPositive(int [],int);

char *InsStr(char *, char *, int);

void OutStr(const char *, int, int);

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

Формат заголовка функции такой же как и формат прототипа, за исключением того, что здесь указывается список формальных параметров (включая их имена) и знак «;» в конце заголовка не ставится.

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

Примеры:

double factorial(int n){

double p=1.0; int i;

for(i=1;i<=n;i++) p*=(double)i; return p;

}

int NumberOfPositive(int arr[], int n){

int s=0,i;

for(i=0;i<n;i++) if(arr[i]>0) s++; return s;

}

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

Использование всех трех составляющих описания функции обусловлено рекомендуемой структурой программы на «С» для программ с несколькими пользовательскими функциями:

  1. Подключение библиотек,

  2. Объявление глобальных типов данных,

  3. Объявление глобальных переменных,

  4. Объявление прототипов функций,

  5. Заголовок и тело функции main,

  6. Заголовки и тела остальных пользовательских функций.

Функция может быть вызвана из функции main, из другой пользовательской функции и из самой себя (рекурсивная функция). Заданный порядок вызова функций создает функциональную схему программы.

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

тип имя1, тип имя2, тип имя3, …

С системной точки зрения параметры на языке «С» могут передаваться двумя способами: в прямом порядке (как в языке «Паскаль») и в обратном порядке (по умолчанию). Для задания прямого порядка в описании функции необходимо указать ключевое слово __stdcall, а для обратного порядка – __cdecl.

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

Например, прототип функции вставки строки в строку будет иметь вид:

char *InsStr(char *, char *, int);

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

При передаче в качестве параметра массива допускается использовать два вида описания: как массива и как указателя. Рассмотрим на примере функции нахождения максимума массива.

Прототип

int GetMax(int [],int);

int GetMax(int *,int);

Заголовок и тело

int GetMax(int a[],int n)

{

int i,m;

m=a[0];

for(i=1;i<n;i++)

if(a[i]>m) m=a[i];

return m;

}

int GetMax(int *a,int n)

{

int i,m;

m=a[0];

for(i=1;i<n;i++)

if(a[i]>m) m=a[i];

return m;

}

При работе со структурами, объединениями и перечислениями никаких ограничений не требуется.

Как и в большинстве других языков программирования, язык «С» позволяет передавать параметры по значению или по ссылке. При передаче параметра по значению в стек заносится непосредственное значение переменной – фактического параметра. В данном случае все модификации, проводимые над этим значением, осуществляются только в вызываемой функции. Для того, что бы изменения переменной происходили и в переменной в вызывающей функции необходимо использовать передачу по ссылке. При таком методе передачи в стек заносится не значение, а адрес переменной и работа в вызываемой функции осуществляется над переменной в вызывающей функции. Параметр по ссылке описывается следующим образом: тип *имя. Т. е. в языке «С» для передачи параметра по ссылке используется синтаксис указателя.

Пример:

void Inc1(int);

void Inc2(int*);

void main()

{

int a=5;

Inc1(a); printf("%d\n",a);

Inc2(&a); printf("%d\n",a);

}

void Inc1(int n) {n++;}

void Inc2(int* n){(*n)++;}

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

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

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

Рассмотрим пример программы:

int Arr[10];

void InArr(); void SortArr(); void OutArr();

void main(){InArr(); SortArr(); OutArr();}

void InArr(){

int i; printf("Input array:\n");

for(i=0;i<10;i++) scanf("%d",&Arr[i]);

}

void SortArr()

{

int i,f=1,r;

while(f){

f=0;

for(i=0;i<9;i++) if(Arr[i]>Arr[i+1]){

r=Arr[i];Arr[i]=Arr[i+1];Arr[i+1]=r;f=1;

}}}

void OutArr(){

int i; printf("Output array:\n");

for(i=0;i<10;i++) printf("%3d ",Arr[i]);

}

В программе объявлен глобальный массив Arr. В функции main содержаться только вызовы дочерних функций. Функция InArr осуществляет ввод массива. В ней описана локальная переменная i – счетчик цикла. Функция SortArr осуществляет сортировку массива, в ней описаны три локальные переменные i, f, r – счетчик цикла, признак завершения сортировки и вспомогательная переменная для сортировки соответственно. Функция OutArr осуществляет вывод массива на экран. Данную программу можно модифицировать, перенеся объявление переменной Arr после функции main и вынеся переменную i перед функциями InArr, SortArr, OutArr.

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

int a=5;

void Show(void);

void main(){int a=6; printf("%d\n",a); Show();}

void Show(void){printf("%d\n",a);}

При выполнении данной программы на экран будет выведено: 6 5. В функции main использовалась локальная переменная, а в функции Show – глобальная.

Подводя итог можно привести два правила:

  1. Локальные переменные описываются внутри функций и видны только внутри них.

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

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

В языке «С» реализованы четыре класса памяти: auto, register, static и extern. Для задания класса памяти переменной необходимо указать его при объявлении:

класс_памяти тип имя_переменной;

Все это время мы работали с классом памяти auto, который используются по умолчанию. Все сказанное выше справедливо для него. Класс register указывает компилятору на то, что данную переменную необходимо по возможности хранить в регистре (индексные переменные). Класс static указывает на то, что переменная должна существовать на всем протяжении выполнения программы. Класс extern указывает на то, что переменная объявлена где-то в другом месте.

Рассмотрим пример программы:

void Show1(void); void Show2(void);

void main(){Show1(); Show1(); Show2(); Show2();}

void Show1(void){int a=1; printf("%d\n",a); a++;}

void Show2(void){static int a=1; printf("%d\n",a); a++;}

При выполнении данной программы на экран будет выведено: 1 1 1 2. При выполнении функции Show1 переменная a создается каждый раз и инициализируется значением «1». При выполнении функции Show2 локальная переменная a создается и инициализируется только один раз. При втором вызове данная переменная уже существует.

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

Созданием переменных управляет компилятор языка «С» и он может оптимизировать этот процесс. Поэтому наиболее часто переменные создаются в момент их первого использования. В режиме отладки при просмотре переменной, которая еще не была создана, выводится сообщение: variable was optimized.

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

тип имя(список фиксированных параметров, ...)

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

int function(int n, ...)

double func(int i, double val, ...)

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

Для доступа к значениям нефиксированных параметров используются типы и макросы для работы со стеком из библиотеки stdarg.h. Работа по извлечению значений нефиксированных параметров заключается в следующем:

  1. объявить переменную типа va_list;

  2. установить ее на последний фиксированный параметр в функции с помощью макроса va_start;

  3. произвести работу с заданным списком значений, используя макрос va_arg;

  4. завершить работу со списком параметров, используя макрос va_end.

Макрос установки переменной для работы со стеком на первый нефиксированный параметр:

va_start(va_list ap, lastfix);

Первым параметром ap макроса является имя переменной для работы со стеком, вторым параметром lastfix – имя последнего фиксированного параметра.

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

va_arg(va_list ap, type);

Первый параметр ap макроса – имя переменной для работы со стеком. Второй параметр type макроса – имя типа получаемого значения (системный или пользовательский тип данных). Макрос автоматически перемещает стековую переменную (параметр ap) на следующее значение.

Макрос для завершения работы со стеком:

va_end(va_list ap);

В качестве параметра ap передается имя переменной для работы со стеком.

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

double Mid(int N,...)

{

va_list ap;

int i = 0;

double S=0;

va_start(ap,N);

while(i<N){

S+=va_arg(ap,double);

i++;

}

va_end(ap);

S/=N;

return S;

}

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

... Mid(3,1.0,2.0,3.0);

а пяти вещественных чисел (1, 2, 3, 4 и 5) –

... Mid(5,1.0,2.0,3.0,4.0,5.0);

В языке С можно объявлять указатели не только на данные, но и на функции. Синтаксис объявления такого указателя следующий:

тип (*имя)(список типов формальных параметров);

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

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

double (*ptr)(int,int);

Установка указателя на функцию осуществляется простым присвоением указателю имени функции:

указатель = имя функции;

Вызов функции через установленный на нее указатель осуществляется так же, как и обычный вызов функции:

указатель(список фактических параметров);

Рассмотрим пример: вывести на экран таблицу значений на промежутке [0,2π] с шагом π/6 одной из функций (1 – sin, 2 – cos). Номер функции задан в целочисленной переменной num. Фрагмент программы:

double (*ptr)(double);

if(num == 1) ptr = sin;

else if(num == 2) ptr = cos;

else return 0;

const double pi = 3.14159265358979323;

double x = 0.0;

while(x <= 2.0*pi){

printf(“func(%lf) = %lf\n”,x,ptr(x));

x += (pi/6.0);

}

В данном примере указатель соответствует прототипам функций sin и cos: обе они принимают один вещественный параметр и возвращают вещественное значение. Вызов функции осуществляется через указатель на нее.

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

тип (*имя[размер])(список типов формальных параметров);

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

Установка указателя на функцию осуществляется простым присвоением элементу массива имени функции:

массив[индекс] = имя функции;

Вызов функции через установленный на нее элемент массива указателей осуществляется так же, как и обычный вызов функции:

массив[индекс](список фактических параметров);

Рассмотрим пример: вывести на экран таблицу значений на промежутке [0,2π] с шагом π/6 одной из функций (1 – sin, 2 – cos, 3 – tan). Номер функции задан в целочисленной переменной num. Фрагмент программы:

double (*ptrs[3])(double);

ptrs[0] = sin; ptrs[1] = cos; ptrs[2] = tan;

if((num < 1)||(num > 3)) return 0;

const double pi = 3.14159265358979323;

double x = 0.0;

while(x <= 2.0*pi){

printf(“func(%lf) = %lf\n”,x,ptrs[num-1](x));

x += (pi/6.0);

}

Функции в языке С могут использоваться рекурсивно. Рекурсия – вызов функцией самой себя. Различают два вида рекурсии:

  • прямая рекурсия;

  • косвенная рекурсия.

Прямая рекурсия – функция вызывает непосредственно саму себя.

Косвенная рекурсия – функция вызывает себя посредством другой функции. Например, функция А вызывает функцию Б, которая, в свою очередь, вызывает функцию А.

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

double Factorial(unsigned n)

{

if(n == 1) return 1.0;

return (double)n*Factorial(n-1);

}

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

Соседние файлы в папке Lab6