ФУНКЦИИ Печат

5. ФУНКЦИИ

ВЪВЕДЕНИЕ

Функциите са основни структурни единици, от които се изграждат програмите в С++. Всяка функция се състои от множество оператори (възможно е това множество да е празно), които се изпълняват като една обобщена операция. Всяка вече дефиниранa (създадена) функция може да бъде извиквана (активирана) и изпълнявана многократно. Всяко извикване на функция е съпроводено с нейното изпълнение. Изпълнението може да стане с различни данни и те се задават при извикването на функцията. В резултат от изпълнението си, функцията може да придобие стойност, която се нарича стойност на функцията или върната стойност.

Всяка С++ програма се състои от една или повече функции. Сред всички функции на една програма задължително трябва да има една и само една функция с име main. Тя определя входната точка в програмата, т.е. тя е първата функция, която се изпълнява при стартиране на програмата.

Добрият стил на програмиране изисква програмите да се изграждат като множество от неголеми функции. Това е свързано със следните предимства:

-  програмата става по-прегледна и по-ясна;

- по-лесно става тестването, настройка и модификация на програмата;

-  програмата става по-кратка, тъй като многократното повторящите се фрагменти от нея се обособяват като функции, които се дефинират еднократно, и след това се извикват многократно;

- постига се икономия на памет, тъй като кодът на дадена функция се съхранява само на едно място в паметта, независимо от броя на нейните изпълнения;

- разработването на всякя функция от една голяма програма може да се възложи на отделен програмист и по този начин да се съкрати времето за разработването й;

-

5.1. ДЕФИНИЦИЯ НА ФУНКЦИЯ

Дефиницията на функцията се състои от две части: заглавна част (прототип) и тяло. Общият вид на дефиницията на функция е следния:

[][]

{

тяло (множество оператори)

}

Заглавната част на дефиницията съдържа следните елементи: модификатор, тип на функцията, име на функцията и списък на формалните параметри. Списъкът на формалните параметри се състои от типовете и имената на параметрите, разделени със запетаи, и е ограден в обикновени скоби. Тялото на функцията се състои от множество оператори, поместени между две фигурни скоби. Ето как изглежда дефиницията на функцията sum, чиято върната стойност е сумата на нейните параметри:

inline int sum(int x,int у)

{

return х+у;

}

Тази дефиниция означава, че sum е име на функция, която връща стойност от тип int и има два параметъра с имена х и у, които са от тип int. Ключовата дума inline е модификатор. Освен него могат да бъдат задавани и други модификатори като overload, virtual, static и пр. Тяхното действие е изяснено по-нататък в тази глава.

Активирането (извикването) на функциите става чрез тяхното име, като формалните параметри се заменят с фактически, например:

sum(2,3);

m=3+sum(2,3);

В този фрагмент функцията sum е извикана два пъти. При първото извикване няма да има никакъв външен ефект, тъй като върнатата стойност на функцията не се използва. При второто извикване, функ­цията sum участвува в аритметичен израз. Нейната върната стойност ще бъде добавена към числото 3 и резултатът ще бъде присвоен на променливата m. Следователно m ще получи стойност 8.

Най-често функциите нямат модификатори, т.е. такива не се указват в заглавната част на дефинициите им. Освен това, типът на върнатата стойност на функциите по подразбиране е int. Следователно, ако една функция връща стойност от тип int, в нейната дефиниция може да не се задава тип на върнатата стойност. Имайки предвид тези особености, функцията sum може да се дефинира по следния начин:

sum ( int х, int y )

{

return х + у;

}

Програма 5.1. Най-проста програма с функция. Функцията намира сумата на двата параметъра.

#include

#include

//Дефиниция на функцията sum

sum(int x,int y)

{

return x+y;

}

//Дефиниция на функцията main

void main()

{

cout<<"Резултат: "<

getch();

}

5.2. ДЕКЛАРАЦИЯ НА ФУНКЦИЯ

Декларацията на функция представлява нейната заглавна част и завършва с точка и запетая. Например, декларацията на функцията sum изглежда така:

sum ( int х, int у );

В декларациите на функциите е задължително задаването само на типовете на формалните параметри, а имената им могат да се пропускат. Следователно функцията sum може да се декларира и по следния начин:

sum ( int, int );

Функциите трябва да се декларират само в случаите, когато извикването им предшества техните дефиниции, както е показано в следната програма:

Програма 5.2. Тази програма е вариант на програма 5.1. Сега функцията sum е разполжена  след функцията main

#include

#include

sum(int,int);                                      //Декларация на функцията sum

//Дефиниция на функцията main

void main()

{

int a,b,s;

couta;

coutb;

s=sum(2,3); //Извикването на sum предшества нейната дефиниция

cout<<"Резултат: "<

getch();

}

//Дефиниция на функцията sum

sum(int x,int y)

{

return x+y;

}

Дефиницията на функцията sum е разположена след дефиницията на функцията main, в която има обръщение към sum. Поради това функцията sum трябва да бъде декларирана преди функцията main. Декларацията на функцията sum носи информация на компилатора, че името sum, което се използува във функцията main, е име на функция с два параметъра от тип int. Ако функцията main беше дефинирана след функцията sum, декларация на функцията sum нямаше да бъде необходима.

5.3. ЗАВЪРШВАНЕ НА ИЗПЪЛНЕНИЕТО НА ФУНКЦИИТЕ. ОПЕРАТОР RETURN

Когато една функция бъде извикана, започва изпълнение на операторите, включени в нейното тяло. Изпълнението на функцията завършва в два случая - при достигане нa затварящата скоба на тялото (изпълнени са всички оператори) или при достигане и изпълнение на оператора return. Синтаксисът на оператора return е следния:

return [];

Изразът, зададен след ключовата дума return, определя върнатата стойност на функцията.

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

При изчисляване на израза, зададен в оператора return, може да се получи резултат, чийто тип се различава от типа на функцията. Тогава типът на изчислената стойност се преобразува до типа на функцията. Ако такова преобразуване е невъзможно, ще бъде регистрирана грешка поради несъвпадение на типовете. Ще поясним това с един пример:

//Преобразуване на типа на върнатата стойнрст

int f ( float x )

{

float y;

. . . . . .

у = х/2;

return у;

}

main()

{

float z;

z = f(9);

cout << z;

}

При извикване на функцията f с параметър 9, променливата у на функцията f ще получи стойност 4.5. Но функцията ще върне стойност 4, тъй като при изпълнение на оператора return ще се извърши привеждане на типа на у в типа на функцията, т.е. float ще се преобразува в int. Следователно на променливата z ще бъде присвоена стойност 4, която ще бъде отпечатана на екрана.

5.4. ФУНКЦИИ, КОИТО НЕ ВРЪЩАТ СТОЙНОСТ

В C++ могат да бъдат дефинирани функции, които не придобиват и не връщат стойност. Тази възможност е предвидена, тъй като има редица операции, които не са свързани с получаването на една стойност. Такива са например размяната на стойностите на две променливи, сортирането (подреждането) на елементите на едномерен масив нарастващ или намаляващ ред, въвеждането или извеждането на някакви данни. Функция, които не връща стойност може да съдържа оператори return,  но не може да съдържа израз след оператор return, a като нейн тип се задава ключовата дума void. Ето един пример:

void in_put_num ( int num )                //Функция, която не връща стойност

{

cout num;

}

Функцията in_put_num не връща стойност, тъй като тя реализира само входнa операция. Поради това тя е дефинирана като void.

5.5. ПРЕДАВАНЕ НА ПАРАМЕТРИТЕ

Функциите взаимодействат с външната по отношение на тях среда по три начина: чрез стойността на функцията, чрез  параметрите на функцията и чрез външните (глобални) променливи. Параметрите се описват в заглавието на функцията. В езика С++ те са три вида:  параметри, предаващи стойност, параметри, предаващи адрес, и параметри псевдоними.

5.5.1. Параметри, предаващи стойност

Параметрите, предаващи стойност могат да бъдат променливи и константи и се описват в заглавието като такива. Това е показано в програма 5.3.

При извикване на функцията, параметър от този вид се замества с израз (променлива и константа са частен случай на израз), а функцията създава параметъра (заделя му място в програмния стек) и го инициализира със стойността на заместващия израз. След излизане от функцията, създаденият параметър се унищожава. Поради това, ако параметърът е заместен с променлива, промяната на стойността на параметъра във функцията не променя стойността на променливата. Нека да разгледаме един пример:

Програма 5.3. Програма с функция с параметър, предаващ стойност

#include

#include

//Дефиниция на функцията func1

int func1 ( int x, const int y )

{

x+=y;

cout <<"x="<

return x;

}

//Дефиниция на функцията main

main()

{

const int b = 5;

int a=10;

cout << "Стойност на функцията: " <

cout <<"a="<

getch();

}

В момента на извикване на функцията func1, параметърът х се замества от а (със стойност 10), а параметърът у – от b (със стойност 5). Следователно след изпълнение на оператора х+=у; (в тялото на функцията func1), стоиността на х ще стане 15 и следващият оператор ще изведе х=15. Операторът return x; прави тази стойност стойност на функцията.

След изпълнението на func1, функцията main извежда на екрана стойността на функцията func1, която е 15, и стойността на а, която обаче е 10, а не 15. От тук следва изводът, че  параметърът, предаващ стойност може да се променя във функцията, но промяната не се отразява на действителния параметър, т.е. функцията работи с копието на променливата а, а не със самата променлива. В тялото на функцията се извежда стойността на копието на параметъра а, която е 10, докато във функцията main се извежда стойността на самата променлива а, коя­то е останала непроменена.

5.5.2. Параметри, предаващи адреси

Параметър, предаващ на функцията адрес, се декларира в заглавието на функцията като указател. При извикване на фyнкциятa, такъв параметър се замества с адрес или указател. В тялото на функцията се използва съдържанието на този адрес (променливата или константата с този адрес), т.е. във функцията се работи направо с променливата или константата с посочения адрес, а не с нейно копие копие.

Работата с параметри указатели е илюстрирана в прог­рама 5.4

Програма 5.4. Програма за размяна на стойностите на две променливи чрез функцията swap с параметри указатели.

#include

#include

//Дефиниция на функцията swap

void swap(int *x,int *y)        //х и у са указатели към int

{

int buf;

buf = *x;

*x = *y;

*y = buf;

}

//Дефиниция на функцията main

main()

{

int p=2,q=3 ;

cout<<"Преди размяната: "<<"р="<swap(&p,&q);//Фактическите параметри са адресите на р и q

cout<<"След размяната:  "<<"р="<getch();

}

На функцията са предадени адресите на р и q като адреси съответно на х и у. Тя променя съдържанието на тези адреси. Но тъй като те са аресите на р и q, функцията всъщност променя стойностите на р и q. Или след изпълнение на функцията swap променливите р и q ще имат стойности 3 и 2, т.е. функцията swap е разменила техните първоначални стойности.

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

5.5.3. Параметри псевдоними

Тези параметри се декларират при дефиницията на функцията като нечии псевдоними, а при извикването на функцията се указва точно чии псевдоними са те. Параметрите псевдоними се декларират по начина, по който се декларират псевдоними, но без да се указват инициализаторите им, защото такива се явяват променливите, които ги заместват при извикване на функцията. В операторите от тялото на функцията се използват псевдонимите, но в действителност се работи със заместващите променливи. Предаването на параметри чрез псевдоними е еквивалентно на предаването чрез указатели, но е по-удобно от гледна точка на използваните синтактични конструкции. Предаването на параметри чрез псевдоними е илюстрирано в програма 5.5. Тя представлява вариант на програма 5.4.

Програма 5.5. Предаване на параметри чрез псевдоними. Програма за размяна на стойностите на 2 променливи.

#include

#include

//Дефиниция на функция swap

void swap ( int &x, int &y )

{

int buf;

buf = x;

x=y;

y=buf;

}

//Дефиниция на функция main

main()

{

int p=2,q=3;

cout<<"Преди размяната: "<<"p="<swap(p,q);

cout<<"След размяната:  "<<"p="<getch();

}

След изпълнение на функцията swap стойностите на променливите р и q ще бъдат разменени, защото функцията чрез псевдонимите x и y всъщност работи с променливите p и q, т.е. разменяйки стойностите на х и у, функцията разменя стойностите на p и q.

При въвеждането на псевдонимите във втората глава беше изяснено, че в някои случаи (когато е възможна конверсия на типовете) типът на един псевдоним и на неговия инициализатор могат да са различни. Ще припомним тази особеност с един пример:

float f = 4.76;

int &x = f;

Поради несъвпадение на типовете на х и f, ще бъде създадена променлива от тип int, която ще бъде инициализирана със стойност 4, колкото е цялата част на стойността на променливата f. Името х ще се отнася до тази нова променлива, а няма да бъде псевдоним на f. Следователно промяната на стойността на х няма да се отразява върху f и обратно. Същата ситуация може да се получи и при предаване на параметри на функциите. Например, функцията swap, дефинирана в програма 5.6, може да бъде извикана с фактически параметри от тип float:

float a = 2.3, b = 3.4;

swap ( a, b );

cout << "a = " << a << " b= " << b << '\n';

В този случай като параметри ще бъдат предадени две променливи от тип int, които се създават при извикване на функцията поради несъвпадение на типовете на фактическите и формалните параметри. Следователно функцията swap няма да размени стойностите на променливите а и b и резултатът, който ще се изведе на екрана ще бъде:

а = 2.3   b = 3.4

Посочената особеност на псевдонимите трябва да бъде добре запомнена, тъй като несъобразяването с нея може да доведе до труднооткриваeми грешки.

5.6. МAСИВИ КАТО ПАРАМЕТРИ НА ФУНКЦИИ

Параметрите на функциите могат да бъдат не само променливи, а и масиви. Но за разлика от променливите, масивите могат да се предават като параметри на функции само чрез указатели и псевдоними.

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

Програма 5.6. Масиви като параметри на функции, предавани чрез указатели. Програма за размяна на стойностите на елементите на два масива.

#include

#include

//Дефиниция на функция swap_arr

void swap_arr ( int *x, int *y, const int len )

{

int buf;

for ( int i = 0; i < len; i++ ){

buf = *(x+i);

*(x+i) = *(y+i);

*(y+i) = buf;

}

}

//Дефиниция на функция main

main()

{

const MAX = 5; //Максимална дължина на масивите r и t

int r[MAX], t[MAX];

//Инициализация на масивите r и t

for ( int i = 0; i < MAX; i++ ){

r[i] = i;

t[i] = MAX - i;

}

swap_arr ( r, t, MAX );

//Извеждане на резултата cout << "РЕЗУЛТАТ:\n";

for ( i = 0; i < MAX; i++ ) {

cout << "r[" << i << "]=" << r[i] << "  ";

cout << "t[" << i << "]=" << t[i] << '\n' ;

}

getch();

}

Функцията swap_arr, дефинирана в тази програма, разменя стой­ностите на елементите на два масива. Тя е обобщен вариант на функцията swap, представена в предишния пример. Параметрите х и у на функцията swap_arr са указатели, които се третират като начални адреси на масиви, а параметърът len съдържа дължината на тези масиви. Функцията main инициализира елементите на два масива r и t, които имат дължина MAX (MAX с константа, дефинирана в main) и извиква функцията swap_arr. При извикване на функцията swap_arr масивите се предават чрез техните имена r и t, които по определение са указатели.

Oбърнете внимание на ключовата дума const, поставена пред декларацията на параметъра lеn на функцията swap_arr. Тя означава, че параметърът len е константа и негоната стойност не може да бъде променяна от функцията. При опит да се промени стойността на len, т.е. ако във функцията swap_arr се използуват изрази от вида lеn++; и len - = 2;, компилаторът ще регистрира грешка.

Програма 5.7. Масиви като параметри на функции, предавани чрез псевдоними. Програма за размяна на стойностите на елементите на два масива.

#include

#include

//Дефиниция на функция swap_arr

void swap_arr ( int x[], int y[], const int len )

{

int buf;

for ( int i = 0; i < len; i++ ){

buf = x[i];

x[i] = y[i];

y[i] = buf;

}

}

//Дефиниция на функция main

main()

{

const MAX = 5; //Максимална дължина на масивите r и t

int r[MAX], t[MAX];

//Инициализация на масивите r и t

for ( int i = 0; i < MAX; i++ ){

r[i] = i;

t[i] = MAX - i;

}

//Извеждане на масивите преди размяната

cout << "ПРЕДИ РАЗМЯНАТА:\n";

for ( i = 0; i < MAX; i++ ) {

cout << "r[" << i << "]=" << r[i] << "  ";

cout << "t[" << i << "]=" << t[i] << '\n' ;

}

swap_arr ( r, t, MAX );

//Извеждане на масивите след размяната

cout << "СЛЕД РАЗМЯНАТА:\n";

for ( i = 0; i < MAX; i++ ) {

cout << "r[" << i << "]=" << r[i] << "  ";

cout << "t[" << i << "]=" << t[i] << '\n' ;

}

getch();

}

Програма 5.7 е еквивалентна на програма 5.6, защото x[ i ] *(x+i) и y[ i ] *(y+i), нo програма 5.7 е по-разбираема.

Програма 5.8. Масиви като параметри на функции, предавани чрез псевдоними. Програма с подпрограми за  въвеждане и сумиране на елементите на едномерен масив.

#include

#include

// Подпрограма за въвеждане на едномерен масив

void in_arr1 (int x[], int &n)

{

if (n==0) {coutn;}

for (int i=0;i

{cout<

}

// Подпрограма за сумиране на едномерен масив

sum_arr1 (int x[], const int n)

{

for (int i=0,s=0;i

s+=x[i];

return s;

}

// Подпрограма main

void main()

{

const N=5; // Максимална дължина на масивa r

int r[N];

int BrEl=0;

in_arr1(r,BrEl);

cout<<"Въведени елементи:\n";

for (int i=0;i

cout<<'\n';

cout<<"Сума: "<

getch();

}

Програма 5.9.  Масиви като параметри на функции. Програма за въвеждане и сумиране на елементите на двумерен масив по редове чрез функцията sum_arr1 от програма 5.8.

#include

#include

// Подпрограма in_arr1 за въвеждане на едномерен масив

void in_arr1 (int x[], int & n)

{

if (n==0) {

coutn;cout<<'\n';

}

for (int i=0;i

cout<

}

}

// Подпрограма sum_arr1 за сумиране на едномерен масив

sum_arr1 (int x[], const int n)

{

for (int i=0,s=0;i

s+=x[i];

return s;

}

// Подпрограмата main

void main()

{

int BrRed,BrCol;

const M=4,N=5;

int r[M][N],suma[M];

cout<<'\n';

coutBrRed;

BrCol=0;

for (int i=0; i

in_arr1(r[i],BrCol);

cout<<"Елементи:";

for (int j=0; j

cout<

suma[i]=sum_arr1(r[i],BrCol); // Сумиране на i-ти ред на масива r

cout<<"Сума: "<

}

getch();

}

Програма 5.10. Масиви като параметри на функции. Програма с подпрограми за въвеждане и намиране на максимален елемент във всеки ред на двумерен масив от символни низове.

#include

#include   // Заради функцията strcmp

#include

// Подпрограма за въвеждане на едномерен масив

void in_arr1 (char x[][5], int &n)

{

if (n==0) {coutn;

}

for (int i=0;i

cout<

}

}

// Подпрограма за намиране на макс. елемент в едномерен масив

max_arr1 (char x[][5], int n)

{

int m=0;

for (int i=1;i

if (strcmp(x[i],x[m])>0) m=i;

return m;

}

// Дефиниране на функцията main

void main()

{int BrRed,BrCol;

const M=4,N=5,K=5;

char r[M][N][K]; int NomMax[M];

cout<<'\n';

coutBrRed;

BrCol=0;

for (int i=0; i

{

in_arr1(r[i],BrCol);

cout<<"Въведени елементи:";

for (int j=0; j

cout<<' '<

NomMax[i]=max_arr1(r[i],BrCol); // Намиране макс. елем. в i-ти ред

cout<<"Mакс. eлемент: "<

// елем.в i-ти ред

}

getch();

}

5.10. ВГРАДЕНИ  ФУНКЦИИ

Едно от предимствата на функциите се състои в това, че техният код се съхранява само на едно място в паметта, независимо от броя на изпълненията им в дадена програма. По този начин силно се намалява разходът на памет. Но икономията на памет е за сметка на бързодействието, тъй като всяко обръщение към дадена функция е свързано с извършването на някои служебни операции (запомняне на възвратния адрес, предаване на параметрите и преход към стартовия адрес на дадената функция и др.), които отнемат известно време. В конвенционални програми подобно забавяне с без значение, но има програми, в които с важно да се постигне максимално бързодействие. Примери за такива програми са програмите за управление на различни устройства и технологични процеси. Програми от този вид работят в режим на реално време, което означава, че работата им трябва да бъде съобразена с различни временни ограничения и съотношения, зависещи от управляваните обекти. Подобни програми понякога могат да бъдат особено чувствителни по отношение на бързодействието.

За да се повиши бързодействието, С++ поддържа т.нар. вградени функции (inline functions). Кодът на тези функции се копира на всяко място в паметта, където има обръщение към тях. По този начин броят на служебните операции, които трябва да се изпълняват при извикване на вградените функции намалява, което води до увеличаване на бързодействието.

Вградените функции се дефинират и използуват както всички останали функции с тази разлика, че дефинициите им започват с ключовата дума inline. Ето един пример:

inline void bell(void)

{

cout << '\a';

}

Изпълнението на тази функция предизвиква издаване на звуков сигнал от зумера на компютъра.

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

5.11. ВИДОВЕ ПРОМЕНЛИВИ. ОБЛАСТ НА ДЕЙСТВИЕ НА ПРОМЕНЛИВИТЕ И ФУНКЦИИТЕ.

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

5.11.1. Автоматични променливи

Автоматични променливи са параметрите на функциите и променливите, дефинирани в телата на функциите. Автоматичните променливи имат две основни характеристики:

- Достъпни (видими) са само за функцията, в коятоса дефинирани, т.е. автоматичните променливи са локални по отношение на функциите.

- Създават се при всяко извикване на функцията, в която са дефинирани и престават да съществуват след нейното завършване.

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

f ( int х, int y )

{

//дефиниции на автоматични променливи

float р = 3.45;    //Инициализация на променливата р

int m = 2, n = 3;                 //Инициализация на променливите m и n

char *s = "string1";         //Инициализация на указател към char

. . . . . . . . . . . . .

. . . . . . . . . . . . .

}

Автоматичните променливи р, m, n и s ще се инициализират наново всеки път, когато функцията бъде извикана, тъй като след завършване на изпълнението на функцията, автоматичните променливи престават да съществуват и техните стойности се губят.

Локалността на автоматичните променливи и временното им съществуване се дължат на механизма на заделяне на памет за тях. Паметта, необходима за автоматичните променливи на дадена функция, се заделя в стека всеки път, когато функцията бъде извикана. Практически това се осъществява чрез преместване на стековия указател на необходимия брой позиции. Докато трае изпълнението на функцията, заделената в стека памет е достъпна за нея. След завършване на изпълнението на функцията, старото положение на стековия указател се възстановява, което е еквивалентно на освобождаване на паметта за автоматичните променливи. Нещо повече, освободената в стека памет се използува за автоматичните променливи на следващата функция, която бъде извикана. Следователно използуването на автоматични променливи води до икономия на памет. Това обаче не е единствената причина, поради която езикът C++, а и други съвременни езици поддържат автоматични променливи. Освен икономия на памет, чрез автоматичните променливи се реализира механизмът на предаване на парамстри на функциите по стойност (параметрите са автоматични променливи). И накрая, без наличие на автоматични променливи не е възможно реализирането на рекурсивни функции.

5.11.2. Външни променливи

Външни променливи са тези, които са дефинирани извън тялото на която и да е от функциите, съставящи дадена програма. (В общия случай външни могат да бъдат не само променливите, но и масивите, структурите и обектите). Външните променливи се различават от автоматичните по следното:

- Външните променливи се създават еднократно при стартиране на програмите и съществуват през цялото време на тяхното изпълнение. Външните променливи не се разполагат в стека, а в друга област от паметта, наречена статична памет.

- Външните променливи са достъпни за множество функции. Възможно е дадена външна променлива да бъде достъпна за всички функции на дадена програма. В този смисъл външните променливи са глобални. Те са аналози на COMMON областите във FORTRAN и на глобалните променливи в PASCAL.

Външните променливи се използуват основно в два случая. Първият от тях се отнася до функции с голям брой параметри. Всяко извикване на такива функции изисква предаване на дълъг списък с фактически параметри, от което програмите стават по-трудно разбираеми. За да се намали броят на параметрите, част от тях могат да бъдат заменени с външни променливи, до които съответните функции имат достъп. Тази стъпка обаче може да се предприеме само в случаите, в които е без значение дали променливите са автоматични или не. Например, параметрите на рекурсивните функции не могат да бъдат заменени с външни променливи, тъй като рекурсията изисква използване на автоматични променливи, както, беше изяснено в предната точка.

Вторият основен случай, в който се използуват външни променливи, се отнася до функции, които работятс обши данни и нямат обръщения една към друга. Тогава, за да се осигури достъп на отделните функции до общите данни, тези данни трябва да се съхраняват чрез външни променливи.

Областта на действие на външните променливи може да обхваща част от даден файл, цял файл или няколко файла. Основното правило, определящо областта на действие на външните променливи, е следното: Областта на действие на една външна променлива се простира от мястото на нейната дефиниция до края на файла. Това означава, че достъп до дадена външна променлива ще имат само функциите, чиито дефиниции са разположени след дефиницията на външната променлина. Дефинирането и използуването на външни променливи е илюстрирано и програма 5.13.

Програма 5.13. Дефиниране и видимост на външни променливи

#include

int х = 1;                   //Дефиниция на външна променлива х

//Дефиниция на функция f1

f1()

{

x++;

cout << "f1: " << "x=" << х << '\n' ;

}

int у = 0;                   //Дефиниция на външна променлива у

//Дефиниция на функция f2

f2()

{

y += х;

cout << "f2: " <<"х=" << х << " у=" << у << '\n';

}

int z = 2;                   //Дефиниция на външна променлива z

//Дефиниция на фунрция f3

f3()

{

z = х + у;

cout << "f3: " <<"х=" << х << " у=" << у << " z=" << z << '\n';

}

//Дефиниция на функция main

main()

{

f1();

f2();

f3();

}

В тази програма са дефинирани три външни променливи - х, у и z. Дефиницията на променливата х се намира преди функциите main, f1, f2 и f3, поради което и всички тези функции имат достъп до нея. Променливата у е достъпна за функциите f2, f3 и main, но не и за функцията f1, тъй като дефиницията на функцията f1 е разположена преди дефиницията на променливата у. И накрая, променливата z е достъпна само за функциите f3 и main. Резултатът от изпълнението на програма 5.13 ще бъде следния:

f1: х = 2

f2: х = 2  у = 2

f3: x = 2   y =2   z = 4

5.11.3. Автоматични и външни променливи с еднакви имена. Оператор за принадлежност ::

Езиците С++ и С допускат наличието на автоматични и външни променливи с еднакви имена. Ако в една функция са дефинирани автоматични променливи, чиито имена съвпадат с имената навъншни променливи, функцията няма да има пряк достъп до външните променливи, тъй като имената на автоматичните променливи "засенчват" имената на външните променливи. В езика С++ (но не и в езика С) този проблем се решава чрез използуване на оператора ::, който се нарича оператор за принадлежност (оригиналният английски термин е scope resolution operator). Поставянето на оператора :: пред името на дадена променлива означава външна променлива с указаното име. Като пример за използуване на оператора :: за разграничаване на външни и автоматични променливи с еднакви имена с показана програма 5.14.

Програма 5.14. Автоматични и външни променливи с еднакви имена. Използуване на оператора ::

#include

int х;                          //Външна променлива

int у = 4;                                  //Външна променлива

//Дефиниция на функция main

main()

{

int x = 2;                             //Автоматична променлива

int у = 3;                            //Автоматична променлива

х += у;

cout << "х=" << х << '\n';

у = ::у + 5;

cout << "у=" << у << '\n';

}

Във функцията main са дефинирани автоматичните променливи х и у, чиито имена съвпадат с имената на външните променливи х и у. Навсякъде в тялото на функцията main имената х и у се свързват с автоматичните променливи. Затова стойността на х след изпълнение на израза х += у; ще бъде 5 (х има стойност 3, а у има стойност 2). Предпоследният ред на функцията main съдържа израза у = ::у + 5;. Операторът ::, поставен пред името у означава, че у e външна променлива. Следователно горният израз се разбира така: На автоматичната променлива у се присвоява сумата от стойностите на външната променлива у и числото 5, т.е. числото 9.

Операторът за принадлежност :: има и други приложения, които са свързани с класовете и обектите и са разгледани в следващата глава.

5.11.4. Разделна компилация. Декларации на променливи и функции

Една програма на С++ може да бъде разположена в няколко файла, които се компилират независимо един от друг, т.е. осъществява се разделна компилация. В резултат на компилацията се получават няколко обектни модула (файлове с разширение .obj). Изпълнимият код напрограмата (файл с разширение .ехе) се получава след свързване на обектните модули. Преимуществото на разделната компилация се състои в това, че при промени в някои от файловете не e необходимо прекомпилиране на останалите. В резултат на това се съкращава времето за компилация, а от там и времето за разработка на програмите.

Когато една програма се състои от множество файлове, възникват ситуации, при които външни променливи, дефинирани в един файл, трябва да бъдат достъпни за функции, дефинирани в други файлове. Такъв е случаят, когато няколко функции са дефинирани в различни файлове, но имат общи данни. Тези общи данни трябва да бъдат външни променливи, до които всички функции да имат достъп. За да се осигури такъв режим на достъп, е необходимо външните променливи да бъдат дефинирани еднократно в някой от файловете, а в останалите файлове да бъдат декларирани като extern (extern е ключова дума). Ще поясним това с един пример: Нека в един файл е дефинирана външната променлива float х. За да бъде тя достъпна и за функции, намиращи се в други файлове, във всеки от тези други файлове трябва да бъде включена декларацията extern float х;. Ключовата дума extern е указание за компилатора, че променливата float х e дефинирана в друг файл. Затова, компилаторът не заделя памет за променливата х, а свързващият редактор търси нейния адрес в друг обектен модул (файл с разширение obj, който се създава от компилатора). Програма 5.15 e конкретен пример за използуване на една и съща външна променлива в три различни файла с имена pr0515.cpp, pr0515a.cpp и pr0515.cpp, които се компилират независимо един от друг.

Програма 5.15. Програма с разделна компилация, вътрешни статични променливи и външни променливи. Отделните елементи на програмата се намират във файловете pr0515.cpp, pr0515а.cpp и pr0515b.cpp

//Съдържание на файла pr0515.cpp:

#include

#include "pr0515a.cpp"

#include "pr0515b.cpp"

# include

extern int x; //x е външна променлива, дефинирана в друг фаил

void f1(); // Декларация на функцията f1

void f2(); // Декларация на функцията f2

main()

{

f1();

f2();

f1();

f2();

f1();

x+=2;

cout<<"x="<< x <<'\n';

getch();

}

//Съдържание на файла pr0515а.cpp:

#include

int x=2; // Дефиниране на външна променлива

void f1()

{

static int y=0;                     // y - статична вътрешна променлива

int z=0;                                                // z - автоматична променлива

x += 2; y+=2; z+=2;

cout <<"x=" << x <<' '<<"y=" << y <<' '<<"z=" << z<< '\n';

}

//Съдържание на файла pr0515b.cpp:

#include

extern int x;          // x е външна променлива, дефинирана в друг фаил

void f2()

{

x += 5;

cout <<"x=" << x << '\n';

}

Резултатът от изпълнението на програмата е:

х = 4

х = 9

х = 9

И трите функции f1, f2 и main използуват външната променлива х, т.е. х играе ролята на общи данни за тези функции. Във файловете pr0515b.cpp и pr0515.cpp e включена декларацията extern int х;, тъй като променливата х e дефинирана във файла pr0515a.cpp, а функциите f2 и main трябва да имат достъп до нея.

Обърнете внимание на декларациите void f1(); и void f2(); във файла pr0515.cpp. В началото на главата беше посочено, че дадена функция трябва да бъде дефинирана или поне декларирана преди мястото на нейното извикване. Тъй като функцията main извиква функциите f1 и f2, които не са дефинирани във файла pr0515.cpp, където има обръщение към тях, то във файла pr0515.cpp са необходими техните декларации. Декларациите на функциите имат същото значение, както и декларацията extern float х;, но когато се декларират функции ключовата дума extern не е необходима.

Декларациите на променливи и функции обикновено се обособяват в отделни файлове, които като правило имат разширение h (header files). Чрез директивата #include на прспроцссора тези файлове се включват във всички други файлове, в които са необходими декларациите. Тази техника има две предимства:

- декларациите не се размножават явно във всеки файл, което намалява възмож­ностите за грешки при въвеждането на текста на програмите;

- при необходимост от добавяне на нови декларации или промяна на старите, не се налага тона да се извършва в множество файлове, а само в един файл - този, който съдържа декларациите.

5.11.5. Статични променливи.

Статичните променливи се дефинират чрез ключовата дума static. Те са два вида: вътрешни и външни. Вътрешните статични променливи са тези, които са дефинирани в тялото на някоя функция, подобно на автоматичните променливи. Външните статични променливи се дефинират извън тялото на която и да е функция, подобно на обикновените външни променливи.

5.11.5.1. Статични вътрешни променливи

Статичните вътрешни променливи имат същата област на действие както автоматичните променливи, т.е. те са достъпни само в рамките на функциите, в които са дефинирани. Но за разлика от автоматичните променливи, статичните вътрешни променливи се създават само веднъж при стартиране на програмите и съществуват през цялото време на тяхното изпълнение. По този белег статичните вътрешни променливи приличат на външните променливи. Накратко, статичните вътрешни променливи са локална постоянна памет на функциите, в които са дефинирани.

Основно приложение на статичните вътрешни променливи е съхранението на данни (стойности), които се получават в резултат от изпълнението на дадена функция и се използуват от същата функция при следващо нейно изпълнение.

Като пример за функция от този вид по-надолу е показана дефиницията на функцията filter, която реализира цифров филтър от първи ред. Цифровите филтри от първи ред се описват със следните две уравнения:

y(t) = а * x(t) + b * y(t-l)

y(t0) = m

където y(t) е стойността на филтрирания сигнал в момента t, x(t) е стойността на зашумения сигнал в момента t, y(t-l) е стойността на филтрирания сигнал в предидущия момент, а константата m е стойността на у в началния момент t0.

Реализацията на тези уравнения може да се осъществи чрез една функция, която трябва да се активира във всеки момент t (в реалния случай през определен интервал от време) и като резултат да връща стойността на y(t). При такава постановка на задачата, стойността на y(t-l), която е необходима за изчисляване на стойността на y(t), ще бъде резултатът от предидущото изпълнение на функцията. Поради тази причина е удобно резултатът от изпълнението на функцията да се съхранява в една статична вътрешна променлива. Ето как изглежда дефиницията на функцията filter, реализираща цифров филтър от първи ред:

float filter ( float a, float Ь, float х, float m, int flag )

{

static float у;        //Статична вътрешна променлива

if ( flag == 0 )        //flag е 0 при първото извикване на функцията

return у = m;                        //y(t0)

return у=а*х+b*у;              // y(t)'

}

Параметрите а и b представляват коефициентите на филтъра, х е стойността на входния (зашумения) сигнал в момента t, a m е стой­ността на изхода на филтъра в началния момент (при първото извикване на функцията). Параметърът flag служи за разграничаване на първото извикване на функцията от следващите нейни извиквания. При първото извикване параметърът flag трябва да има стойност 0, а при следващите - ненулева стойност.

5.11.5.2. Статични външни променливи.

Статичните външни променливи се дефинират извън тялото на която и да е функция, като дефиницията им започва с ключовата дума static. Областта им на действие съвпада с областта на действие на обикновените външни променливи, т.е. от мястото на дефиницията им до края на файла. Но за разлика от обикновените външни променливи, статичните вьншни променливи не могат да бъдат декларирани като extern в други файлове и следователно са недостъпни за функции, дефинирани в други файлове.

5.11.6. Статични функции

Функциите също могат да бъдат статични, което се указва чрез ключовата дума static, поставена пред дефинициите им. Областта на действие на статичните функции съвпада с тази на статичните външни променливи, т.е. статичните функции са видими само във файла, в който са дефинирани. Тъй като видимостта на статичните функции и на статичните външни променливи е ограничена в рамките на един единствен файл, то в други файлове могат да бъдат дефинирани външни променливи и функции със същите имена и прототипи.

Чрез статичните външни променливи и функции може да се реализира идеята за "скриване на данните" (data hiding), която е в основата на модулното програмиране. Например, ако в един файл бъдат обособени няколко статични външни променливи и статични функции, то достъпът до тези променливи ще се осъществява само и единствено чрез споменатите функции. По този начин могат да се оформят различни модули със строго регламентиран достъп до данните. В програма 5.16 е показан един такъв модул, съдържащ данни и функции, чрез които се реализпра опашка.

Програма 5.16. Реализация на опашка. Статични променливи и функции

//Съдържание на файл pr0516.cpp

#include

#include

#include "pr0516a.cpp"

#include              //Заради toupper()

void dispatch ( char ); //Декларация на функция dispatch, koяmo е дефинирана

//във файла pr0516а.cpp

//Дефиниция на функцията main

main()

{

char com;

do {

cout<< "I - Въвеждане O - Извеждане C - Нулиране Q - Изход:";

cin>> com;

com=toupper(com);        //Преобразуване в главни бykвu

cout<<'\n';

dispatch(com);

} while (com != 'Q');

getch();

}

//Съдържание на файл pr0516а.cpp - модул, сьдържащ данни и функции, реализиращи //onaшka

#include

#define BOOLEAN   int

#define TRUE     1

#define FALSE                    0

const MAX=5;                                    //Максимална дължина на опашката

//Дефиниции на статични данни

static int  first = 0;                               //Първи елемент в опашката

static int  last =  0;                              //Последен елемент в опашката

static int  count = 0;                           //Брой елементи в опашката

static int   buf[MAX];                        //Буфер на опашката

//Статична функция put_last - вмъква елемент в опашката

static BOOLEAN put_last(int &val)

{

if ( count < MAX ){          //Ako onaшкama не е пълна

count++;

buf[last] = val;

if ( ++last > MAX-1 ) last = 0;

return TRUE;

}

else return FALSE; //Пълна onaшкa

}

//Статична функция get_first - извлича елемент от опашката

static BOOLEAN get_first( int &val )

{

if ( count == 0 )

return FALSE;                                //Празна опашка

else {

count--;

val = buf[first];

if ( ++first > MAX-1 ) first = 0;

return TRUE;

}

}

//Статична функция clear - изчистване на опашката

static void clear()

{

count = first = last = 0;

}

//Дефиниция на функция dispatch - интерфейс за работа с опашката

void dispatch(char com)

{

int value;

switch ( com ){                                  //com съдържа подадената кoмaндa

case 'I':                                                //Вмъкване на елемент

cout value;

if( put_last(value))

cout << "OK\n\n";

else cout << "\nПълна onaшкa!\n\n";

break;

case 'O':                                              //Извличане на елемент

if (get_first(value))

cout<<"\nРезултат :"<

else cout << "Празна onaшкa\n\n";

break;

case 'C' :                                              //Изчистване на опашката

clear();

break;

case 'Q': break;

default: cout<<"\nНеправилна кoмaндa\n\n";

}

}

Програма 5.16 се състои от два файла – pr0516.cpp и pr0516a.cpp. Файлът pr0516a.cpp представлява модул, чрез който с реализирана опашка. Оптиката е структура от данни, подчиняваща се на дисциплината "първи влязъл, първи излязъл", известна ощe като дисциплина FIFO ( Fifsl In, Fifst Out). Опашката се реализира чрез един масив, наречен buf, в който се съхраняват елементите на опашката и два индекса first и last, които са съответно номерът на първия и номерът на последния елемент в опашката. Променливата count съдържа текущия брой на елементите в опашката. Константата МАХ определя максималния брой елементи, които може да побере опашката. За да не се усложнява примерът, в програма 5.16 е реализирана опашка, чиито елементи са числа от тип int.

Работата с опашката се осъществява чрез три функции - put_last, get_first и clear, реализиращи съответно операциите вмъкване на елемент в опашката, извличане на елемент от опашката и изтриване на всички елементи от опашката (изпразване на опашката). Всички изброени функции и променливи са статични, т.е. областта на действието им е само файла pr0516a.cpp, в който те са дефинирани. Единствената функция, дефинирана в този файл pr0516a.cpp, която не е статична е функ­цията dispatch. Тя извиква една от функциите put_last, get_first или clear в зависимост oт стойността на параметъра com. Този параметър има смисъл на команда и се въвежда от клавиатурата във функцията main, която е дефинирана във файла pr0516.cpp и има достъп единствено до функцията dispatch.

Файлът pr0516а.cpp може да се разглежда като един модул, който има определена вътрешна структура и поведение (статичните променливи и функции) и интерфейс (функцията dispatch), чрез който се осъществява взаимодействието с външната среда. Вътрешната структура на модула е недостъпна за никоя функция, освен за функцията dispatch. По този начин се постигат два положителни ефекта:

-   вътрешните данни на опашката (buf, first, last, count) са предпазени от некоректна работа с тях;

- потребителят спазва определена дисциплина за работа с опашката, което намалява възможностите за грешки.

5.12. ФУНКЦИИ, КОИТО ВРЪЩАТ УКАЗАТЕЛИ

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

type *f(...........)

{

. . . . . . . . .

return ;

}

5.13. УКАЗАТЕЛИ КЪМ ФУНКЦИИ

В С++ и С могат да бъдат дефинирани указатели, чиито стойности не са адреси на данни, а начални адреси на функции. Такива указатели се наричат указатели към функции. Ето как изглежда дефиницията на един указател към функция:

float (*funcpoint)(int, float);

Тази дефиниция означава, че funcpoint е указател към функция, която има два параметъра от тип int и float и връща стойност от тип float. Обърнете внимание на скобите, ограждащи указателя funcpoint! Ако те липсват, дефиницията ще означава, че funcpoint е функция, която връща указател към float.

На указателите към функции могат да се присвояват началните адреси на различни функции, след което извикването на функциите да става чрез указателите. Началните адреси на функциите се означават чрез имената на функциите, без да се поставят скоби след тях и без да се задават параметри. Например, нека е дефинирана една функция с име f1:

float f1(int х, float y)

{

. . . . . . . . . . . .

}

Началният адрес на функцията f1 може да бъде присвоен на указателя funcpoint чрез израза:

funcpoint = f1;

От този момент, функцията f1 може да бъде извиквана чрез указателя funcpoint no следния начин:

(*funcpoint)(5, 3.4);

Някои компилатори, в това число и BORLAND C++, допускат извикването на функция чрез указател да става само чрез името на указателя, а не чрез съдържанието на указателя, поставено в скоби. Имайки предвид това, вместо (*funcpoint) (5, 3.4), може да се използува по-простият запис funcpoint(5,3.4).Чрез указателя funcpoint могат да бъдат извиквани и други функции, като преди това трябва да му бъдат присвоявани техните начални адреси.

Указателите към функции намират приложение предимно в два случая. Първият случай е свързан с разработването на драйвери за различни устройства, с които работи дадена програма. Драйверите се оформят като отделни функции, а използуването им от програмата става чрез указатели към функции. По този начин се постига независимост на програмата от конкретните драйвери в даден момент. Освен това добавянето на нови драйвери не изисква промени в програмите, които ги използуват. Вторият, често срещан случай на използуване на указатели към функции, е свързан с предаването на функции като параметри на други функции. За да може една функция да се предаде като параметър на друга функция, то съответният формален параметър трябва да бъде указател към функция. Програма 5.18 илюстрира този случай.

Програма 5.18. Табулация на функция. Предаване на функция като параметър

#include

//Дефиниция на функция tabul

void tabul ( float (*f) (float), float y[], float x[], int count, float step = 1 )

{

for ( int i = 0; i < count; i++ ) y[i] = (*f)( x(i] = i*step );

}

//Дефиниция на функция f1

float f1 ( float х )

{

return x*x+2;

}

//Дефиниция на функция f2

float f2 ( float х )

{

return x*x-l;

}

//Дефиниция на функция main

main()

{

const MaxLen = 5;     //Максимална дължина на масивите

float Х[MaxLen], y[MaxLen];

tabul ( f1, y, х, MaxLen, 2 );

cout << "РЕЗУЛТАТ 1:\n";

for ( int i = 0; i < MaxLen; i++ ){

cout << "x[" << i << "]=" << x[i] << "  ";

cout << "y[" << i << "]=" << y[i] << '\n';

}

tabul ( f2, y, x, MaxLen );

cout << "РЕЗУЛТАТ 2:\n";

for ( i = 0; i < MaxLen; i++ ) {

cout <<  "x[" << i << "]=" << x[i] << " ";

cout <<  "y[" << i << "]=" << y[i] << '\n';

}

}

Функцията tabul осъществява табулация на функция (математическа) на един аргумент. Функцията, която се табулира, се предана като първи параметър на функцията tabul. След изпълнение на функцията tabul, масивите у и х съдържат съответно стойностите на функцията, която се табулира (предава се като първи параметър на функцията tabul) и стойностите на аргумента, за които е изчислена функцията. Параметърът count съдържа броя на стойностите в масивите у и х. В програма 5.18 масивите са дефинирани във функцията main, като дължината им е зададена чрез константата MaxLen. Параметърът step задава стъпката на изменение на стойностите на аргумента на функцията, която се табулира. Параметърът step има подразбираща се стойност единица. Следователно, ако при извикване на функцията tabul параметърът step не бъде зададен, то неговата стойност ще бъде едно (виж т. 5.14.). Във функцията main, функцията tabul се извиква два пъти, за да се табулпрат две функции с имена f1 и f2. функциите f1 и f2 съответствуват на две математически функции, които имат следните аналитични записи: f1(x) = х*х+2 и f2(x) = х*х-1. При първото извикване на функцията tabul параметърът step има стойност две. При второто извикване параметърът step не се предава. Следователно функцията f2 ще бъде табулирана със стъпка единица, колкото с подразбиращата се стойност на параметъра step.

Някои компилатори, в това число и BORLAND C++, допускат опростено деклариране на указателите към функции като формални параметри на функциите. Опростяването се състои в премахване на скобите и * пред имената на указателите към функции. Следвайки това правило, функцията tabul може да бъде дефинирана и по този начин:

void tabul(float f(float),float y[],float x[].int count,float step=1)

{

for ( int i = 0; i < count; i++ )

y[i] = f( x[i] = i*step );

}

5.14. ФУНКЦИИ С ПОДРАЗБИРАЩИ СЕ ПАРАМЕТРИ

Функциите в С++ могат да имат подразбиращи се параметри, т.е. параметри, на които са зададени подразбиращи се стойности. За задаване на подразбиращи се стойности на параметрите се използува знака за присвояване. Ето един пример:

float func ( int х, float у = 2.3, char *s = "string1" )

{

cout << "x=" << х << " y=" << у << " s=" << s << ' \n';

}

Параметрите у и s на функцията func са подразбиращи се и техните подразбиращи се стойности са съответно 2.3 и "string1". Тези стойности ще бъдат предадени на функцията като параметри, ако при нейното извикване не бъдат зададени други стойности на подразбиращите се параметри. Тъй като функцията func има два подразбиращи се параметъра, тя може да бъде извикана по три различни начина - с един, с два и с три параметъра. Например,

func ( 2 );

func ( 2, 3.0 );

func ( 2, 4.0, "string2" );

Резултатите от- тези три изпълнения на функцията func ще бъдат съответно:

х=2             у=2.3      s= string1

х=2             у=3.0      s= string1

х=2             у=4.0      s= string2

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

int f ( int х, float у = 1.2, char *s ) //ГРЕШКА!

{

.............

}

е некоректна, тъй като вторият паряметър у е подразбиращ се, а параметърът s, които се намира след него в списъка на формалните параметри, не е подразбиращ се.

5.16. РЕКУРСИВНИ ФУНКЦИИ

Рекурсията е механизъм, при които една функция може да извика сама сeбe си. Функциите, които използуват рекурсия, се наричат рекурсивни. Има два варианта на рекурсия, наречени пряка и непряка (косвена) рекурсия. Пряка рекурсия има тогава, когато в тялото на дадена функция има извикване на същата тази функция, т.е. функцията пряко вика себе си. Косвена рекурсия има тогава, когато една функция извиква друга функция, а тази друга функция на свой ред извиква първата. Косвената рекурсия може да бъде и на повече нива, например една функция вика втора функция, втората функция вика трета функция, а третата функция вика отново първата функция.

Рекурсивните функции са особено подходящи, когато трябва да се реализират алгоритми, които са рекурсивни по определение. Типични примери за такива алгоритми са изчисляване на факториел, изчисляване на числата на Фибуначи, обработка на дървовидни структури данни и др. Като пример за рекурсивна функция ще разгледаме изчисляването на n!. То се извършва по известната формула:

   1,                                      aкo n=0  или n=1

n! =        

   n*(n-1)!,                          акo n>1

Тази формула се реализира от рекурсивната функция fact, дефинирана по следния начин:

fact ( int n )

{

if ( n == 0 || n == 1 ) return 1;

return n * fact(n-l);          //Buкa себе си, т.e. пряка рекурсия

}

Докaзано е, че всяка рекурсивна функция има итеративен вариант, т.е. всеки рекурсивен алгоритъм може да се реализира без използуване на рекурсивни функции. Препоръчваме на читателите да напишат итеративна функция, която да изчислява n! и да я сравнят с рекур­сивната функция fact, показана по-горе.

В основата на рекурсията са автоматичните променливи, които се създават при всяко извикване на дадена функция. Ако няма автоматични променливи, то ново извикване на дадена функция преди завършване на нейното предидущо извикване би предизвикало разрушаване на данните, които са необходими за завършване на предидущото извикване. Ще поясним този проблем на базата на функцията fact. Функцията fact има един параметър n, който е автоматична променлива. Нека функцията fact бъде извикана с фактически параметър две: fact(2);. При извикването в стека ще бъде заделена памет за параметъра n и в тази памет ще се запише стойността 2. Тъй като условието на оператора if не е изпълнено (n е 2), ще се достигне до оператора return n*fact(n-l);. Неговото изпълнение изисква изчисляване на израза n*fact(n-l);. Следователно отново ще бъде извикана функцията fact, но с параметър, чиято стойност е n-1, т.е. 1. При това второ извикване на функцията fact в стека ще бъде заделена нова памет за нейния параметър, която е различна от паметта, заделена за параметъра на функцията при нейното първо извикване. Следователно параметърът n от първото извикване на функцията запазва своята стойност, която е 2. При второто извикване на функцията условието на оператора if ще бъде изпълнено, тъй като n има стойност 1. Следователно ще бъде изпълнен оператора return 1;, с което ще завърши изпълнението на второто извикване на функцията fact. Тогава ще продължи изпълнението на оператора return от първото извикване на функцията. Върнатата стойност от второто извикване на функцията fact, която е 1, ще бъде умножена по стойността на параметъра n от първото извикване на функцията, която е 2 и полученият резултат, който е 2, ще бъде резултатът от извикването на функцпята fact(2).

Ако параметърът n не е автоматична променлива, т.е. ако при всяко извикване на функцията не се заделя нова памет за него, функцията fact няма да работи правилно, тъй като стойността на n от първото извикване на функцията Ще бъде променена след нейното второ извикване. Това ще доведе до неверен резултат от изчислението на израза, зададен в оператора return.

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

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

Програма 5.20 Програма с рекурсивна функция за пресмятане на N!

#include

#include

// Дефиниране на функцията swop

int fakt (int N)

{

if (N==0||N==1) return 1;

return N*fakt(N-1);

}

// Дефиниране на функцията main

main()

{

int M,Z;

coutM;

Z=fakt(M);

cout<

getch();

}

Програма 5.25. Програма с рекурсивна функция за сумиране елементите на едномерен масив

#include

#include

int arr[5];

// Дефиниране на функцията sum_arr

int sum_arr (int n)

{

if (n==0) return arr[0];

return arr[n] + sum_arr(n-1);

}

// Дефиниране на функцията main

main()

{

int i,s,N;

coutN;

cout<<"Задайте елементите на масива\n";

for (i=0;i

cout<<"i="<

}

s=sum_arr(N-1);

cout<<"Сумата на елементите е:"<

getch();

}