Лабораторная работа № 7 Разработка и отладка алгоритмов и программ с применением пользовательских функции.
Цель. Научиться разрабатывать алгоритмы и программы с применением пользовательских функций, осуществлять выбор способа обмена данными.
Основные теоретические сведения
Создание и использование функций
Принципы программирования на языке Си основаны на понятии функции. Уже были рассмотрены некоторые функции: printf( ), scanf( ), getchar( ), putchar( ). Эти функции являются системными, однако мы создали и несколько собственных функций под общим именем main( ). Выполнение программы всегда начинается с команд, содержащихся в функции main( ), затем последняя вызывает другие функции. Рассмотрим вопрос, как создавать свои собственные функции и делать их доступными для функции main( ), а также для других функций.
Функция – это самостоятельная единица программы, спроектированная для реализации конкретной задачи. Вызов функций приводит к выполнению некоторых действий. Например, при обращении к функции printf( ) осуществляется вывод данных на экран. Функции могут выполнять различные действия и получать значения величин, используемых в программе.
Использование функций избавляет от многократного «переписывания» кода. Если конкретную задачу в программе необходимо выполнить несколько раз, можно написать соответствующую функцию только один раз, а затем по необходимости вызывать ее кроме того мы можем применять одну функцию, например putchar( ), в различных программах.
Многие программисты думают о функции, как о "черном ящике". Они задают ее через поступающую информацию и полученные результаты. Все, что происходит внутри черного ящика, их не касается. Таким образом, все, что такому программисту нужно знать о конкретной, уже заранее реализованной, функции это: как ее можно определить, как к ней обратиться и как установить связи между функцией и программой, ее вызывающей.
Абстракция управления в языке Си обеспечивается с помощью функций. Все функции могут быть рекурсивными (т.е. вызывать сами себя). В языке Си отсутствуют подпрограммы (процедуры), однако возврат функцией значения в вызывающую программу не обязателен, следовательно, функции могут быть разделены на две категории - функции, возвращающие значения, и функции, не возвращающие значения в вызывающую программу (подпрограмму).
Определение функций, возвращающих значение, имеет следующий простейший формат:
тип-результата имя-функции (формальные аргументы)
{
тело функции
}
Пример:
float f(float x){
float y;
y=x*x;
return y;
}
или
float f(float x){
return x*x;
}
Указание типа-результата функции в языке Си не является обязательным. Если тип результата не указан, то предполагается, что результат имеет тип int. Поскольку указание типа функции приводит к большей ясности и легкости чтения программы, а также упрощает нахождение в ней ошибок, тип функции всегда должен быть указан явно.
В качестве результата функция не может возвращать массив или функцию, но может возвращать указатель на массив или функцию. Выполнение функции, возвращающей значения, обязано завершиться операцией return вида
return e;
которая обеспечивает передачу результата e.
Функция, возвращающая значение, может содержать более одной операции return, однако только в различных ветках операций ветвления, например,
int f(int a){
if (a>5) return a;
else return 0;
}
Если функция не возвращает значения, то в заголовке функции указывается тип void.
Например:
void f(int a){
for (int i=0;i<a;i++)printf("Hello world");
}
В такой функции наличие операции return необязательно. Если return все же присутствует, то после него не должно указываться никаких параметров:
return;
Оператор return оказывает и другое действие. Он завершает выполнение функции и передает управление следующему оператору в вызывающей функции. Это происходит даже в том случае, если оператор return является не последним оператором тела функции:
int abs(int x)
{
if(x < 0) return(-x);
else return(x);
printf("Работа завершена!\n");
}
Наличие оператора return препятствует тому, чтобы оператор печати когда-нибудь выполнился в программе.
Вызов функций
Вызов функции производится следующим образом:
f(b);
или таким
int t = f(b);
В первом случае результат передается системе и не сохраняется в переменной.
Все функции в программе, написанной на языке Си, равноправны. Каждая из них может вызывать любую другую функцию и, в свою очередь, каждая может быть вызвана любой другой функцией. Это делает функции языка Си несколько отличными от процедур Паскаля, поскольку процедуры в Паскале могут быть вложены в другие процедуры, причем процедуры, описанные в разных процедурах, являются недоступными для процедур, описанных в других независимых процедурах.
У функции main есть специфика. Она заключается в том, что после сборки программы, состоящей из нескольких функций, ее выполнение начинается с первого оператора функции main( ).
Параметры функции.
Различают понятия аргументов и результатов функции. Результаты – это возвращаемые функцией данные в основную программу, аргументы – это данные, передаваемые в функцию. Любые данные, которые записываются после имени функции в скобках, будем называть параметрами функции. Совокупность параметров, их типов, а также специализированных ключевых слов будем называть сигнатурой функции.
Формальный параметр – параметр, который описывается в заголовке функции и значение которого считается неопределенным до вызова данной функции. Фактический параметр – параметр, который передается в функцию при ее вызове (под фактическими параметрами мы в данном случае будем подразумевать и константы или сложные выражения). Независимо от типа фактического параметра он вначале вычисляется, а затем его величина передается функции. Фактический параметр - это конкретное значение, которое присваивается переменной, называемой формальным параметром.
Если для связи с некоторой функцией требуется более одного аргумента, то наряду с именем функции можно задать список аргументов, разделенных запятыми. Например:
print_num(int i, int j)
{
printf("значение i=%d значение j=%d", i,j);
}
Обращение в программе к данной функции будет таковым:
print_num(6,19);
либо таковым:
int k = print_num(6,19);
Обратите внимание, аргументы функции при ее объявлении нельзя записать так: print_num(int i, j)
В языке Си допустимы функции, количество аргументов у которых при компиляции функции не фиксировано. Количество и тип аргументов становится известным только в момент вызова функции, когда явно задан список фактических аргументов (параметров). При определении и описании таких функций, имеющих списки параметров неопределенной длины. Спецификация формальных параметров заканчивается запятой и многоточием
Спецификация явных параметров - список спецификации параметров, количество и типы которых фиксированы и известны в момент компиляции. Эти параметры обязательны. Каждая функция с переменным количеством параметров должна иметь хотя бы один обязательный параметр. После списка явных (обязательных) параметров ставится запятая, а затем - многоточие. Компилятор знает, что дальнейший контроль соответствия количества и типов параметров при обработке вызова функции проводить не нужно. Чтобы функция с переменным количеством параметров могла воспринимать параметры различных типов, необходимо в качестве исходных данных каким-то образом передавать ей информацию о типах параметров.
Пример:
#include <stdio.h>
/* Функция суммирует значения своих параметров */
long summa(int m,...) /*m - число параметров*/
{
int *p=&m; /*настроили указатель на параметр m*/
long t=0;
for(;m>=0;m--) t+=*(++p);
return t;
}
void main()
{
printf("\n summa(2,6,4)=%d",summa(2,6,4));
printf("\n summa(6,1,2,3,4,5,6)=%d", summa(6,1,2,3,4,5,6));
Передача параметров в функцию и из нее в языке Си может осуществляться двумя способами:
по значению
по указателю
В языке Си++ добавляется еще один способ передачи параметров – по ссылке.
Рассмотрим передачу аргументов и результатов по значению. В этом случае передается не сама переменная, а ее копия, что абсолютно не влияет на значение переменной вне данной функции.
int a = 5;
int f(int a){
a+=3;
return a;
}
int k = f(a);
В данном случае в результате a= 5, k= 8.
Если же осуществлять передачу параметров по указателю, то в данном случае изменяется и содержимое внешней по отношению к функции переменной.
int *a;
int = 5;
void f(int *a){
(*a)+=3;
}
f(&a);
int k = *a;
В данном случае в результате a= 8, k= 8. Обратите внимание, что в функцию необходимо передать адрес переменной (&)
Передача по ссылке является, фактически, упрощенной формой передачи параметра по указателю.
int a = 5;
void f(int &a){
a+=3;
}
f(a);
int k = a;
Обратите внимание, что в данном случае не нужно писать значек «*». Результат, по прежнему, a= 8, k= 8
Передача нескольких результатов
Передача аргументов по ссылке и указателю может пригодиться в случае если функция должна возвращать несколько результатов. Как мы указали выше в функции не может содержаться две и более операции return, если они не входят в конструкцию ветвления. Также параметры функции не могут указываться через запятую после операции return. Кроме того, в языках Си/Си++ отсутствуют подпрограммы (процедуры), а существуют только функции. В классическом понимании, функция не может возвратить больше одного результата. Как выйти из данного положения, если необходимо возвратить несколько результатов. В этом случае поступают, например так:
void f(int a, int *r1, int r2){
(*r1)+=a;
(*r2)-=a;
}
или в языке Си++ так:
void f(int a, int &r1, int &r2){
r1+=a;
r2-=a;
}
где r1 и r2 результаты функции.
Кроме того, возможен следующий вариант:
int f(int a, int &r1){
r1+=a;
r2-=a;
return r2
}
Существует еще один вариант передачи результатов: при помощи структуры.
struct myStruct {
int r1;
int r2;
};
myStruct ms;
myStruct f(int a){
ms.r1+=a;
ms.r2-=a;
return ms;
}
Локальные переменные
В первом приближении будем считать, что переменные, которые описаны внутри тела функции, называются локальными. Переменные, которые описаны вне данной функции будут являться глобальными по отношению к данной функции. В дальнейшем мы дополним данные определения. Мы подчеркиваем, что локальные переменные являются действительно локальными. Даже в том случае, когда мы используем одно и то же имя для переменных в двух различных функциях, компилятор считает их разными переменными.
Это означает, что формальные параметры (аргументы) функции локализованы в ней, то есть недоступны вне определения функции и никакие операции над формальными параметрами в теле функции не изменяют значения фактических параметров.
Следует учесть, что не рекомендуется, например, следующая запись:
int *f(){
int local = 5;
return &local;
}
Переменная local является локальной поэтому невозможно вернуть указатель на данную переменную, т.к. ее не существует вне данной функции. Вместе с тем, если записать так:
int f(){
int local = 5;
return local;
}
то такое объявление вполне законно, т.к. в точку вызова функции передается копия локальной переменной.
Передача массивов в качестве параметров
Существует несколько способов передачи массивов в качестве параметров функции, однако наиболее универсальным является передача массивов по указателю. Итак, условимся, что массивы будем передавать в функцию только через указатель:
int a[3]={2,5,-6};
int f(int *a){
if (*(a+1)==5) return 1;
else return 0;
}
Возможна и следющая запись:
int a[3]={2,5,-6};
int f(int *a){
if (a[1]==5) return 1;
else return 0;
}
Вызов функции будет, например, следующим:
f(a);
Обратите внимание, т.к. массив уже является указателем на его нулевой элемент, то знак & при вызове функции писать не нужно.
Передавать многомерные массивы лучше всего следующим способом:
void print_mij(int *m, int dim1, int dim2)
{
for(int i=0;i<dim1;i++){
for(int j=0;j<dim2;j++)
cout << m[i*dim2+j] ;
cout << endl;
}
}
Однако, если размерность массива известна заранее, то можно передать массив и следующим способом:
void print_m35(int m[3][5])
{
for(int i=0;i<3;i++){
for(int j=0;j<5;j++)
cout << m[i][j] ;
cout << endl;
}
}
Массивы передаются в любом случае как указатель, а не копируются
Возвращение значений в качестве результата функции по ссылке и указателю.
Существует возможность возврата указателя или ссылки в операции return. Не стоит только забывать, что нельзя возвращать указатель или ссылку на локальную переменную.
Прототипы функций.
Для более ясной структуры программы, а также для удобства обработки компилятором функций использовать предварительные объявления функций, так называемые прототипы.
Пример.
void f(int a);
void f(int a){
for (int i=0;i<a;i++)printf("Hello world");
}
имя переменной в прототипе можно опустить:
void f(int);
void f(int a){
for (int i=0;i<a;i++)printf("Hello world");
}
Использование прототипов может быть выгодно в следующем случае:
void f(){
g(3);
}
void g(int a){
for (int i=0;i<a;i++)printf("Hello world");
}
В данном участке кода будет выдана ошибка, т.к. функцию g компилятор еще не «знает», а она уже вызывается в функции f. Для нормальной работы программы следует написать так:
void g(int a){
for (int i=0;i<a;i++)printf("Hello world");
}
void f(){
g(3);
}
либо так:
void g(int);
void f(){
g(3);
}
void g(int a){
for (int i=0;i<a;i++)printf("Hello world");
}
Обратите внимание, в конце описания прототипа ставится «;»