Глава 4: Функции и обхват

Съдържание на четвърта глава :

4.1. Рекурсия
4.2. Функции inline
4.3. Строга проверка на типовете
4.4. Връщане на стойност
4.5. Списък от аргументи на функция
4.6. Изпращане на аргументи
4.7. Аргумент - псевдоним (reference)
4.8. Аргумент - масив
4.9. Програмен обхват
4.10. Локален обхват




--------------------------------------------------------------------------------



За функциите може да се мисли като за потребителско дефинирани операции. Най-общо казано функцията се представя чрез име, а не чрез оператор. Операндите на функцията, наречени нейни аргументи, се задават от затворен в скоби списък от аргументи, разделени чрез запетаи. Резултатът на функцията се нарича нейн тип за връщане. Функция, която не връща стойност,има тип за връщане void.

Фактическите действия, които реализира една функция, са описани в тялото й. Тялото на функцията се затваря във фигурни скоби (“{}”) и понякога се нарича блок. Ето няколко примера за функции

intline int abs( int i )

{ // return the absolute value of i

return( i < 0 ? -i; i );

}

inline int min( int v1, int v2 )

{ // return the smaller of two values

return ( v1 < v2 ? v1 ; v2 );

}

gcd( int v1, int v2 )

{ // return greatest common denominator

int temp;

while ( v2 )

{

temp = v2;

v2 = v1 % v2;

v1 = temp;

}

return v1;

}

Една функция се изпълнява, когато към името й се приложи операторът за извикване на функция ("()"). Ако функцията очаква да получи аргументи, тези аргументи, наречени фактически аргументи на извикването, се поставят в оператора за извикване на функция. Аргументите се отделят със запетаи. Това се нарича изпращане на аргументи на функция. В следващия пример main() извиква abs() два пъти, min() и gcd() по веднъж. Тя е описана във файла main.C.

#include <stream.h>

#include "localMath.h"

main(){

int i, j;

cout << "Value "; // get value from standart input

cin >> i;

cout << "Value ";

cin >> j;

cout << "\nmin " << min( i, j ) << "\n";

i = abs( i );

j = abs( i );

cout << "gcd " << gcd( i, j ) << "\n";

}

При обръщение към функция се извършва едно от две възможни действия. Ако функцията е била декларирана като inline, по време на компилация в точката на обръщение се извършва заместване на обръщението с тялото на функцията; иначе функцията се извиква по време на изпълнение. Обръщението към функция предизвиква предаване на управлението на извиканата функция изпълнението на текущата активна функция се преустановява. Когато приключи изчислението на извиканата функция прекъснатата функция продължава изпълнението си от точката, непосредствено следваща повикването. Управлението на извикването на функции се осъществява с помощта на програмния стек, създаван по време на изпълнение.

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

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

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

За да бъде компилирана main.C функциите abs(), min() и gcd() трябва първо да бъдат декларирани; иначе всяко от повикванията им в тялото на main() ще предизвика грешка по време на компилация.

Трите прототипа имат вида (не е необходимо да се задават имената на имената на аргументите, а само типа им)

int abs( int );

int min( int, int );

int gcd( int, int );

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

Заглавният файл може да бъде наречен localMath.h. Той може да има следния вид (inline функциите са дефинирани в заглавния файл, а не в текстов файл на програмата)

int gcd( int, int );// inlines are placed within header file

inline abs( int i)

{ return( i<0 ? -i ; i ); }

inline min( int v1, int v2)

{ return( v1 <= v2 ? v1 ; v2 ); }

Компилацията на програмата се извършва по следния начин $ CC main.C gcd.C

След изпълнение на програмата се получават следните резултати

Value 15

Value 123

min 15

gcd 3



4.1. Рекурсия

Функция, която прави обръщение към себе си директно или индиректно, се нарича рекурсивна. Функцията gcd(), например, може да бъде написана отново като рекурсивна

rgcd( int v1, int v2 )

{ if (v2 == 0 )return v1;

return rgcd( v2, v1%v2 ); }

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

Извикването

rgcd( 15, 123 );

връща стойност 3. В таблица 4.1 са описани последователните стъпки при изпълнението.

Последното извикване rgcd(3,0) удовлетворява условието за спиране. Тя връща най-големия общ делител - 3. Тази стойност става връщаната стойност на всички предишни обръщения. Казва се, че тази стойност се промъква нагоре.

v1
v2
return

15
123
rgcd(123, 15)

123
15
rgcd( 15, 3)

15
3
rgcd( 3, 0)

3
0
3



Таблица 4.1 Стъпки при изпълнение

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

Факториел на едно число се пресмята като произведение на последователността от числа от 1 до числото. Например, факториела на 5 е 120; т.е.1 * 2 * 3 * 4 * 5 = 120

Пресмятането на факториела на едно число изисква само по себе си рекурсивно решение.

unsigned longfactorial ( int val )

{

if ( val > 1 )

return val * factorial( val - 1);

return val;

}

В този случай условието за спиране е val да има стойност 1.

Упражнение 4-1. Напишете factorial() като итеративна функция.

Упражнение 4-2. Какво ще се случи ако условието за спиране има вида if ( val != 0 )

Упражнение 4-3. Как мислите, защо връщаната стойност на функцията е дефинирана от тип unsigned long, докато аргументът е от тип int?



4.2. Функции inline

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

min( i, j );

вместо

i < j ? i ; j;

Ползата от дефинирането й като функция се състои в следното:

- много по-лесно се чете извикването на функцията min() отколкото един аритметичен if, особено когато i и j са сложни изрази.

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

i <= j

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

- съществува единна семантика в програмата. За всяка проверка се гарантира еднотипна реализация.

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

- функциите могат да бъдат използувани повторно по-скоро, от-колкото да се пишат отново за други приложения. Съществува, обаче, една основна пречка за дефинирането на min() като функция тя ще бъде значително по-бавна. Трябва да се копират два аргумента, трябва да се запазят машинните регистри, програмата трябва да се разклони към ново място. Затова написването на кода е просто по-бързо.

int minVal = i <= j ? i ; j;

intVal1 = min( i, j );

Функциите inline предлагат едно решение. Всяка функция inline се разширява “на реда” в точката на повикването си.

intVal1 = min( i, j );

се записва по време на компилация като

intVal1 = (i <= j) ? i ; j;

Извикването на функцията min() по време на изпълнение се отстранява.

Функцията min() се декларира като inline чрез ключовата дума inline в дефиницията. Трябва да отбележим,обаче, че спецификацията inline е само една препоръка за компилатора. Една рекурсивна функция, такава като gcd(), например, не може напълно да разшири inline (въпреки, че нейното първо повикване би могло да бъде разширено). Вероятно функция с дължина 1200 реда няма да бъде разширена inline. Изобщо механизмът inline е средство за оптимизиране на малки няколкоредови често извиквани функции.



4.3. Строга проверка на типовете

Функцията gcd() очаква два аргумента от тип int. Какво ще се случи ако й бъдат подадени аргументи от тип float или char*? Какво ще се случи ако се изпрати само един аргумент или повече от два?

Основните операции, които gcd() изпълнява над двата си аргумента са от модулната аритмитика. Модулната аритметика не може да се прилага за нецели операнди.Следователно обръщението

gcd( 3.14, 6.29 );

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

gcd( "hello", "world" );

Или от

случайното слепване на двете стойности в това обръщение?

gcd( 24312 );

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

gcd( "hello", "world" ); //error invalid argument

//types (char*, char*)// -expecting (int, int)

gcd( 24312 );//error missing value for argument two

Какво се случва, когато в обръщението участвуват два аргумента от тип double? Отбелязването на това обръщение като свързана с типовете грешка е правилно, но може би много строго. По-скоро аргументите може неявно да бъдат конвертирани към int, като така се задоволят изискванията на списъка от аргументи. Понеже това е стесняващо конвертиране ще появи предупреждение. Обръщението добива вида

gcd( 3, 6 );

и връща стойност 3.

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



4.4. Връщане на стойност

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

double sqrt( double );

char strcpy( char, const char* );

IntArray &Intarray qsort();

TreeNode *TreeNode inOrder();

void error(const char* ... );

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

Следните две декларации на isEqual() са еквивалентни; и двете описват типа на връщаната от функцията стойност като int

int isEqual( long*, long* );

isEqual( long*, long* );

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

return;

return expression;

Тази способност за проверка на типовете се счита за особено ценна, така че комисията ANSI за езика С е възприела прототипа на функция от С++ за езика ANSI С.

Операторът return не задължителен за функции, които са декларирани от тип void. Използува се обикновено за да предизвика прекратяване на изпълнението на функцията. (Този вид използване на оператора return съответствува на използването на оператора break в циклите). Едно неявно изпълнение на return се получава при достигане на последния оператор на функцията. Например,

void dCopy( double *scr, double *dst, int sz )

{ // copy scr array into dst

// simplifying assumption arrays are same size

if ( scr == 0 || dst == 0 ) // if either array is empty, quit

return;

if ( scr == dst ) // no need copy

return;

if( sz <= 0 ) // nothing to copy

return;

// still here@ copy.

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

dst[ i ] = scr[ i ];

// no explicit return necessary

}

Втората форма на оператора return определя резултата на функцията. Той може да бъде произволен сложен израз; може да съдържа и обръщение към функция. Реализацията на функцията factorial(), например, съдържа следния оператор return

return val * factorial( val-1 );

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

enum Boolean { FALSE, TRUE };

Boolean isEqual ( char *s1, char *s2 )

{// if either are null, not equla

if ( s1 == 0 || s2 == 0 ) return FALSE; // if s1 == s2, return

// TRUE; else FALSE

if ( s1 == s2 ) // the same string

return TRUE;

while ( *s1 == *s2++ )

if (*s1++ == ‘\0’ ) return TRUE;

// still here not equal

return FALSE;

}

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

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

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

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



4.5. Списък от аргументи на функция

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

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

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

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

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

- няма никаква локализация на грешките.

- много трудно се пишат правилни рекурсивни програми когато функциите използват глобални променливи.

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

Пропускането на аргумент или изпращането на аргумент от неправилен тип са източници на сериозни грешки по време на изпълнение на програма, написана на предишния ANSI C език. Със въвеждането на строгата проверка на типовете, тези интерфейсни грешки почти винаги се откриват по време на компилация. Вероятността за възникване на грешка при изпращане на аргументите се увеличава с увеличаване на размера на списъка от аргументи - някои функции на FORTRAN приемат до 32 аргумента. Като едно общо правило може да се приеме, че броят на аргументите не трябва да бъде повече от осем. Ако една функция се нуждае от повече арагументи то вероятно тя се опитва да направи прекалено много неща; един по-добър проект би могъл да я раздели на две или повече по-специализирани функции.

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

1. Значително намалява сложността на списъка от аргументи.

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

Синтаксис на списъка от аргументи

Списъкът от аргументи на функция не може да бъде пропускан. Функция, която не получава аргументи може да се опише или с празен списък от аргументи или със списък, съдържащ единствено ключовата дума void. Например, следните две декларации на fork() са еквивалентни

// equivalent declarations

int fork();

int fork( void );

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

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

min( int v1, v2 ); // error

min( int v1, int v2 ); // ok

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

print( int *array, int size );

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

Специалната сигнатура многоточие ...

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

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

foo( arg_list, ... );

foo( ... );

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

Функцията printf() от стандартната изходна библиотека на С е пример за това, кога е необходимо многоточието. printf() винаги получава символен низ като първи аргумент. Дали тя ще получи и други аргументи се определя от първият й аргумент, наречен форматиращ низ. Метасимволите, зададени чрез %, показват че съществуват и допълнителни аргументи. Например,

printf( "hello, world\n" );

получава като аргумент единствен низ. Обаче,

printf( "hello, %s\n", userName );

получава два аргумента. Символът % показва, че съществува и втори аргумент, а s показва, че типа на аргумента е низ. printf() е декларирана в С++ по следния начин

printf( const char* ... );

Според това описание при всяко извикване на printf() трябва да бъде изпратен един аргумент от тип char*. След това могат да бадат подавани каквито и да е аргументи.Следните две декларации не са еквивалентни

void f();

void f( ... );

В първият случай f() е декларирана като функция, която няма аргументи;

във втория - като функция с нула или повече аргументи. Обръщенията

f( someValue );

f( cnt, a, b, c );

са правилни само за втората декларация. Обръщението f(); е правилно и за двете функции.

Специалната сигнатура инициализация по подразбиране

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

За потребителя на операционната система UNIX, например, всеки текстов файл, създаден от него, се дефинира по подразбиране с разрешение за четене и запис за него и само за четене - за останалите. Ако ние желаем да разширим или стесним правата на достъп до файла, като системата UNIX поддържа прост механизъм за модифициране или заменяне на подразбиращите се стойности. Дадена функция може да определя подразбиращи се стойности за един или повече от аргументите си използувайки синтаксиса на сигнатурата. Функция, която създава и инициализира двумерен символен масив за да симулира екран на терминал може да зададе стойности по подразбиране за височината, ширината и вида на основата за символите на екрана

char * screenInit( int height = 42, int width = 80,

char background = ‘ ‘);

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

char *cursor;

// equivalent to screenInit(24, 80,’cursor = screenInit()‘);

// equivalent to screenInit(66, 80,’ cursor = screenInit( 66 )‘);

// equivalent to screenInit(66, 256,’ cursor = screenInit( 66, 256)‘);

cursor = screenInit( 66, 256, ‘#’);

Забележете, че не е възможно да зададете стойност на background без да определите height и width. Такова свързване на аргументите се нарича позиционно. Част от работата по проектирането на една функция се състои в това да бъдат подредени аргументите в сигнатурата така, че стойността, която е най-вероятно да бъде инициализирана от потребителя да се намира на първо място. Допускането при проектирането на screenInit() (достигнато вероятно на основата на експерименти) е, че height е тази стойност, която най-вероятно ще бъде задавана от потребителя.

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

ff( int = 0 ); // in ff.h

#include "ff.h";

ff( int i = 0 ); { ... } // error

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

Успешната декларация на една функция може да определи допълнителни подразбиращи се инициализатори - това е един полезен метод за пригодяване на една обща функция към специфично приложение. Функцията chmod() от системната UNIX библиотека променя защитата на даден файл. Прототипът на функцията се намира в системния заглавен файл stdlib.h. Той е деклариран по следния начин

chmod( char *filePath, int protMode );

където protMode определя режима на защита на файл, а filePath представя името и пътя до местоположението на файла. Някакво частно приложение винаги променя режима на защита на файловете си на read-only. За да не се указва това всеки път chmod() се декларира повторно за да поддържа стойност по подразбиране

#include <stdlib.h>

chmod( char *filePath, int protMode = 0444 );

Даден е следния прототип на функции, деклариран в заглавен файл

ff( int a, int b = 0, int c ); // ff.h

Как можем да декларираме отново ff() в някакъв наш файл, така че b да има подразбираш се инициализатор? Написаното по-долу е правилното представя подразбираш се инициализатор

#include "ff.h"

ff( int a, int b = 0, int c); // ok

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

#include "ff.h"

ff( int a, int b = 0, int c);

// ok

ff( int a = 0, int b, int c);

// ok



4.6. Изпращане на аргументи

За функциите се записва информaция в една структура, наречена програмен стек от времето на изпълнение. Тази информация остава в стека дoкато функцията е активна. След като функцията приключи изпълнението си тази информация се изтрива автоматично. Цялата област, отделена за информацията, се нарича запис на активиране.

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

Подразбиращият се в С++ метод за инициализация при изпращането на аргументи е чрез копиране на стойностите за четене (rvalue) на фактическите аргументи в областта, отделена за формалните аргументи. Това се нарича изпращане по стойност. При изпращането по стойност функцията никога няма достъп до фактическите аргументи на обръщението. Стойностите, които функцията обработва са нейни собствени локални копия; те са записани в стека. Изобщо, промените направени над тези стойности не се отразяват на стойностите на фактическите аргументи. Когато функцията приключи работата си и записа на активирането бъде изтрит тези локални стойности се изгубват. При изпращането по стойност съдържанието на фактическите аргументи не се променя. Това означава, че програмистът не е длъжен да запазва и възстановява стойностите на аргументите, когато прави обръщение към функция. Без механизма за изпращане по стойност може да се предполага, че всеки формален аргумент, който не е деклариран от тип const може да бъде потенциално изменен при всяко извикване на функцията. Извикването по стойност има минимален потенциал за нанасяне на щети и изисква минимум усилия от потребителя. Изпращането по стойност е един разумен механизъм за изпращане на аргументи по подразбиране.

Изпращането по стойност, обаче, не е подходящо за всяка функция. Механизмът за изпращане по стойност не е подходящ за следните случаи:

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

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

void swap( int v1, int v2)

{ int tmp = v2;

v2 = v1;

v1 = tmp; }

swap() разменя локалните копия на аргументите си. Фактическите променливи, изпратени на swap(), остават непроменени. Това се илюстрира от следната програма, която вика swap()

#include <stream.h>

vpid swap( int, int);

main()

{ int i = 10;

int j = 20;

cout << "Before swap()\ti "<< i << "\\tj" << j << "\n";

swap( i, j );

cout << "After swap()\ti "<< i << "\\tj" << j << "\n"; }

След като компилираме и изпълним тази програма ще получимследния резултат

Before swap()

i 10 j 20

After swap()

i 10 j 20

За програмиста съществуват две алтернативи на механизма изпращане по стойност. В първия случай формалните аргументи се декларират като указатели (pointer). Тогава функцията swap() може да бъде написана така

void pswap( int *v1, int *v2)

{ int tmp = *v2;

*v2 = *v1;

*v1 = tmp; }

main() трябва да бъде модифицирана така, че да декларира и извиква pswap(). Програмистът вече трябва да изпраща адресите на двата обекта, а не самите обекти

pswap( &i, &j );

Когато компилираме и изпълним тази програма, показаните резултати вече ще бъдат правилни

Before swap()

i 10 j 20

After swap()

i 20 j 10

Когато желаете само да избегнете копирането на даден аргумент, декларирайте го като const

void print( const BigClassObject* );

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

void rswap( int &v1, int &v2 );

{ int tmp = v2;

v2 = v1;

v1 = v2; }

Обръщението към rswap() от main() изглежда така, както и обръщението към оригинала swap()

rswap( i, j );

След като тази програма бъде компилирана и изпълнена ще се види, че стойностите на i и j са правилно разменени.



4.7. Аргумент - псевдоним (reference)

Този аргумент изпраща на функцията стойността за запис на фактическия аргумент. Използуването му има следните ефекти:

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

2. Не съществуват ограничения за изпращането на големи класови обекти като аргументи на функция. Когато се използува механизма за изпращане по стойност се копира целия обект при всяко обръщение към функцията.

Ако аргументът от псевдонимен тип не се променя във функцията се препоръчва да бъде деклариран като const. Забележете, обаче, че е неправилно декларирането на х като const в следния пример

class X;

int foo( X& );

int bar( const X& x )

{ // const passed to nonconst reference

return foo ( x ); // error }

x не може да бъде изпратен като аргумент на foo() освен ако сигнатурата на foo() не бъде променена на const X& или X. Псевдонимен аргумент от компактен тип или от тип с плаваща запетая може да се държи неочаквано, когато фактическият аргумент не му съответствува точно по тип. Това се дължи на факта, че се генерира временен обект, на който се присвоява стойността за четене на фактическия аргумент, и тогава този обект се изпраща на функцията. Например, ето какво ще се случи, когато извикате rswap() с аргумент unsigned int

int i = 10;

unsigned int ui = 20;

rswap( i, ui );

Това обръщение се интерпретира така

int T2 = int(ui);

rswap( i, T2 );

Изпълнението на това обръщение към rswap() дава следния неправилен резултат

Before swap()

i 10 j 20

After swap()

i 20 j 20

ui остава непроменена, понеже тя никога не се изпраща на rswap(). По-скоро се изпраща T2 - временно генерирания обект поради несътветствието на типовете. В резултат се моделира изпращането по стойност. (Компилаторът трябва да издаде предупреждение).

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

void prswap( int *&v1, int *v2 )

{ int *tmp = v2;

v2 = v1;

v1 = tmp; }

Декларцията int *&p1; трябва да бъде четена от ляво на дясно. p1 е псевдоним на указател към обект от тип int. Променената реализация на main() ще изглежда така

#include <stream.h>

void prswap( int *v1, int *&v2 );

main()

{

int i = 10;

int j = 20;

int *pi = &i;

int *pj = &j;

cout << "Before swap()\tpi "<< *pi << "\\tpj" << *pj << "\n";

prswap( pi, pj );

cout << "After swap()\tpi "<< *pi << "\\tpj" << *pj << "\n";

}

Когото компилираме и изпълним тази програма ще получим следния резултат

Before swap()

i 10 j 20

After swap()

i 20 j 10

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

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

int IntArray

operator[]( int index )

{ return ia[ index ]; }

Intarray myAarray[ 8 ];

myArray[ 2 ] = myArray[ 1 ] + myArray[ 0 ];

Раздел 2.8 съдържа дефиницията на класа IntArray.



4.8. Аргумент - масив

Масивите в С++ никога не се изпращат по стойност. По-скоро масивът се подава чрез указател към първия му елемент. Например,

void putValues( int[ 10 ] )]

се разглежда от компилатора сякаш е било декларирано като

void putValues( int* );

Безмислено е да се декларира размера на масив, когато се подава като формален аргумент. Следните три декларации са еквивалентни

// three equivalent declarations of putValues

void putValues( int* );

void putValues( int[] );

void putValues( int[ 10 ] );

За програмиста това означава следните две неща

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

Размерът на масива не е част от типа на аргумента. Функцията, която има аргумент масив не знае неговият фактически размер;

това важи и за компилатора. Няма проверка за размера на масива. Например,

void putValues( int[ 10 ] ); // treated as int*

main()

{

int i, j[ 2 ];

putValues( &i ); // ok

int*;

run-time error putValues( j ); // ok

int*;

run-time error return 0;

}

Проверката на типа на аргумента просто потвърждава, че двете обръщения към putValues() са с аргумент от тип int*.

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

void petValues9 ( int[], int size );

main()

{ int i, j[ 2 ];

putValeus( i, 1 );

putValues( j, 2 );

return 0;}

putValues() отпечатва стойностите на масива в следния формат

( 10 ) < 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 >

където 10 представлява размера на масива. Ето една реализация

#include <stream.h>

const lineLength = 12; // elements to a line

void putValues( int *ia, int sz )

{

cout << "(" << sz; ") <";

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

{

if ( i % lineLength == 0 && i )

cout << "\n\t"; // line filled

cout << ia[ i ]; // seperate all but last element

if ( i % lineLength != lineLength-1 &&i != sz - 1 )

cout << ", ";

}

cout << " >\n";

}

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

void putValues( int matrix[][10], int rowSise );

декларира matrix като двумерен масив. Всеки ред на matrix се състои от 10 елелента. Една еквивалентна декларация на matrix има вида

int (*matrix) [10];

Това декларира matrix като указател към масив от 10 елемента.

matrix += 1;

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

int *matrix[ 10 ];

декларира matrix като масив от десет указателя към цели числа.



4.9. Програмен обхват

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

Един втори, по-общ контекст е обхвата. С++ поддържа три вида обхват: файлов, локален и класов. Файловият обхват се отнася за тази част от текста на програмата, който не се съдържа във функция или дефиниция на клас. Той представлява най-външният обхват на програмата. Може да се каже, че той обхваща както локалния, така и класовия обхват.

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

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

1. Счита се, че всеки клас представя ясно разграничен класов обхват.

2. Членовете - функции се разглеждат като включени в обхвата на класа.

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

int i = 0;

int j;

Стойността на една неинициализирана локална променлива не е дефинирана.

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

void swap( int *ia, int, int);

void sort( int *ia, int sz );

void putValues( int *ia, int sz );

int ia[] = { 4, 7, 0, 9, 2, 5, 8, 3, 6, 1 };

const SZ = 10;

main()

{

int i, j;// ...

swap( ia, i, j );

sort( ia, SZ );

putValues( ia, SZ );

}

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

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

1. Една локална променлива може да използува име на глобална променлива. Тогава се казва, че глобалната променлива е скрита от локалнияпредставител. Как програмистът може да получи достъп до скритата глобална променлива? Едно решение предлага оператора за обхват (scope -оператор).

2. Нека една променлива е дефинирана в даден файл, но се използува също и в друг или множество други файлове. Как програмистът декларира променливата в тези файлове? Едно решение предлага ключовата дума extern.

3. Нека две глобални променливи са дефинирани в различни програмни файлове и имат еднакви имена, но възнамеряваме да ги използуваме като различими обекти. Всяка от тях сама по себе си се компилира нормално. Когато ги компилираме заедно, обаче, се отбелязва, че са дефинирани неколкократно и се прекъсва процеса на компилация. Как програмистът може да компилира програмата без да му се налага да променя всички представители на една отпроменливите? Използуването на ключовата дума stаtic предлага едно решение.Тези три случая са обект на обсъждане в следващите три раздела.



Оператор за обхват (scope)

Операторът за обхват предлага едно решение на проблема за достъп до скрита глобална променлива. Идентификатор, предществуван от оператор за обхват, ще ви осигури достъп до глобален представител. В следващия пример, проектиран да илюстрира как може да бъде използуван оператора за обхват, една функция изчислява числата на Фибоначи. В нея са зададени две дефиниции на променливата max. Глобалният представител на променливата съдържа максималната стойност за числата от поредицата. Локалният й представител показва желаната дължина на редицата. (Да припомним, че формалните аргументи на една функция се включват в локалния й обхват). Двата представителя на max трябва да бъдат достъпни във функцията. Всяко явно обръщение към max, обаче, ще бъде отнесено към локалния представител. За да имаме достъп до глобалния представител трябва да използуваме оператора за обхват - max. Ето една реализация

#include <stream.h>

const max = 65000;

const lineLength = 12;

void fibonacci( int max )

{

if (max < 2 ) return; cout << "0 1 ";

for ( int i = 3, v1 = 0, v2 = 1, cur; i << max; ++i)

{ cur = v1 + v2;

if ( cur > max ) break;

cout << cur << " ";

v1 = v2;

v2 = cur;

if ( i % lineLenggth == 0 ) cout << "\n";

} }

Ето и една реализация на main(), отнасяща се до примерната функция

#include <stream.h>

void fibonacci( int );

main()

{

cout << "Fibonacci Series 16\n";

fibonacci( 16 );

return 0;

}

Когато компилираме и изпълним тази програма ще получим следния резултат

Fibonacci Series

16 0 1 1 2 3 5 8 13 21 34 55 89 144 233 337 610



Променливи extern

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

extern int i;

дава “обещание”, че някъде в програмата съществува дефиниция от вида

int i;

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

Ключовата дума extern може да бъде записвана и в прототип на функция. Единственият ефект на това ще бъде, че неявната й характеристика “дефинирана другаде” ще стане явна за прототипа. Това се прави по следния начин

extern void putValues( int* , int );

Декларацията на променлива extern в явен инициализатор се разглежда като дефиниция на тази променлива. Отделя се памет и всяка следваща дефиниция на тази променлива в същия обхват се отбелязва като грешна. Например,

extern conts bufSize = 512; // definition

Глобални променливи static

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

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

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

bsort(), qsort() и swap().

Предназначението на bsort() и qsort() е да сортират масив във възходящ ред. Функцията swap() се вика и от двете функции, но не са предназначени за обща употреба. За нашия пример те са декларирани като static. (Друга алтернатива е да бъдат декларирани като inline).

static void swap ( int *ia, int i, int j )

{ // swap two elements of array

int

tmp = ia[ i ];

ia[ i ] = ia[ j ];

ia[ j ] = tmp;

}

void bsort( int *ia, int sz )

{ // bubble sort

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

for ( int j = i++; j < sz; j++ )

if ( ia[ i ] > ia[ j ] ) swap( ia, i, j );

}

1 void qsort( int *ia, int low, int high )

2 { // stopping condition for recursion

3 if ( low < high )

4 { int lo = low;

5 int hi = high + 1;

6 int elem = ia[ low ];

7

8 for (;;)

9 { while ( ia[ ++lo ] <= elem );

10 while ( ia[ --hi ] > elem );

11

12 if ( lo < hi )

13 swap( ia, lo, hi );

14 else break;

15 } // end,

16 for(;;)

17 swap( ia, low, hi - 1 );

18 qsort( ia, low, hi - 1 );

19 qsort( ia, hi + 1, high );

20 } // endq if ( low < high );

21 }

qsort() е една реализация на алгоритъма за бързо сортиране на C.A.R Hoare. Нека разгледаме подробно тази функция. low и high задават долната и горната граница на масива. qsort() е една рекурсивна функция, която прилага към себе си прогресивно намаляващи подмасиви. Условието за приключване на работа е когато долната граница е равна (или по-голяма) от горната граница (ред 3).

elem (ред 6) се нарича разделящ елемент. Всички елементи на масива, които са по-малки от elem се преместват от лявата му страна; а всички по-големи елементи - отдясно. Сега масивът е разделен на два подмасива. qsort() се прилага рекурсивно към всеки от тях (редове 18 - 19).

Предназначението на цикъла for(;;) е да извърши разпределянето на елементите (редове 8 - 15). При всяка итерация на цикъла lo се увеличава, докато не достигне индекса на първия елемент на ia, който е по-голям от elem (ред 9). Съответно hi се намалява, докато не достигне най-крайния индекс на елемент, който е по-малък или равен на elem (ред 10). Ако lo вече не е по-малко от hi елементите са били разпределени и ние прекъсваме изпълнението на цикъла; иначе елементите се разменят и започва следващата итерация (редове 12 - 14).

Въпреки, че масивът е бил разпределен, elem все още се намира в ia[low]. Функцията swap() от ред 17 го поставя на окончателната му позиция в масива. qsort() се прилага тогава към двата подмасива.

Следната реализация на main(), която изпълнява двете сортиращи функции, използува putValues() при отпечатването на масива.

#include <stream.h>

#include "sort.h" /* bsort(), qsort() */

#include "print.h" /* putValues() */

// for illustration, predefine arrays

int ia1 [ 10 ] = { 26, 5, 37, 1, 61, 11, 59, 15, 48, 19 } ;

int ia2 [ 16 ] = { 503, 87, 512, 61, 908, 170, 897, 275, 653,

426, 154, 509, 612, 677, 765, 703 } ;

main()

{

cout << "\nBubblesort of first array";

bsort( ia1, 10 );

putValues( ia1, 10 );

cout << "\nQuicksort of second array";

qsort( ia2, 0, 15 );

putValues( ia2, 16 );

}

Когато компилираме и изпълним тази програма ще получим следния резултат

Bubblesort of first array ( 10 )

< 1, 5, 11, 15, 19, 26, 37, 48, 59, 61 >

Quicksort of second array( 16 )

< 61, 87, 154, 170, 275, 426, 503, 509, 512, 612, 653, 677, 703, 897, 908 >



4.10. Локален обхват

Най-общо казано, за локалните променливи се отделя памет при обръщението към функцията; за тях се казва, че “идват във” или че имат “входен обхват”. Тази памет се отделя по време на изпълнение от програмния стек и е част от записа за активиране на функцията. Една неинициализирана локална променлива ще съдържа случайна стойност, според битовата последователност, записана в паметта при предишното й използуване. За тази стойност се казва, че не е дефинирана. Когато една функция приключи работата си, от програмния стек се отстранява записа й за активиране. По такъв начин паметта, свързана с локалните променливи се освобождава. Казва се, че променливите “излизат вън” или, че имат “изходен обхват” при приключване на изпълнението на функцията. Стойностите, които те съдържат, се загубват.

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

#include <streama.h>

const strLen = 8;

char *globalString;

char *trouble()

{

char localString[ strLen ];

cout << "Enter string ";

cin >> localString;

return localString; // dangerous

}

main()

{ globalString = trouble(); ... }

globalString съдържа адреса на локален масив от символи localString. За нещастие, обаче, паметта, отделена за localString се освобождава, когато функцията trouble() приключи изпълнението си. Когато се върнем отново в main(), globalString фактически адресира неизползувана памет.

Когато адресът на дадена локална променлива се изпраща вън от обхвата й се казва, че става дума за “висящ указател”. Това е една сериозна програмна грешка, понеже съдържанието на адресираната памет е непредсказуемо. Ако битовете на този адрес се окажат сравнително подходящи, програмата може да се изпълни до край, но да даде грешни резултати.

Локалните обхвати могат да бъдат влагани. Всеки блок, който съдържа декларативни оператори поддържа свой собствен локален обхват. Например, следната част от програма дефинира две нива на локален обхват, реализирайки двоично търсене в сортиран масив.

const notFound = -1;

int binSearih( int *ia, int sz, int val )

{ // local scope level #1// contains ia, sz, val, low, high

int low = 0;

int high = sz - 1;

while ( low <= high )

{ // local scope level #2

int mid = ( low + high )/2;

if ( val == ia[ mid ] )

return mid;

if ( val < ia[ mid ] ) high = mid - 1;

else low = mid + 1;

} // end, local scope level #2

return notFound;

} // end, local scope level #1

Цикълът while от binSearch() дефинира вложен локален обхват. Той съдържа един идентификатор, идентификатора mid, затворен в локалния обхват на binSearch(). Той съдържа аргументите ia, sz и val, както и локалните променливи high и low. Глобалният обхват затваря и двата локални обхвата. Той съдържа един идентификатор, цялата константа notFound.

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

Използуването на оператора за обхват (“ “) ограничава търсенето до глобален обхват.

Един цикъл for разрешава дефинирането на променливи в управляващата си структура. Например,

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

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

int i;

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

Това осигурява достъп на програмиста до управляващите променливи

след приключване на изпълнението на цикъла.Например,

const notFound = -1;

findElement( int *ia, int sz, int val )

{

for ( int i = 0; i < sz, ++i )

if ( ia[ i ] == val ) break;

if ( i == sz ) return notFound;

return i;

}

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

fooBar( int *ia, int sz )

{

for (int i=0; i<sz; ++i) ... // defines i

for (int i=0; i<sz; ++i) ... // error i redefined

for (i=0; i<sz; ++i) ... // ok

}



Локални променливи static

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

Този проблем може да бъде решен чрез деклариране на идентификатора като static. За всяка локална променлива static се отделя постоянна памет. Нейната стойност остава след извикването; но достъпът до нея остава ограничен в локалния й обхват. Например, ето една версия на gcd(), която проследява дълбочината на рекурсията, използувайки локална променлива static

#include <stream.h>

traceGcd( int v1, int v2 );

{

static int depth = 1;

cout << "depth #" << depth++ << "\n";

if (v2 == 0 ) return v1;

return traceGcd( v2, v1%v2 );

}

Стойността, свързана със static локалната променлива depth остава след извикването на traceGcd(). Инициализация се извършва само веднъж. Следната малка програма показва изпълнението на traceGcd()

#include <stream.h>

extern traceGcd(int, int);

main()

{

int rslt = traceGcd( 15, 123 );

cout << "gcd of (15, 123) " < rslt << "\n";

}

Когато компилираме и изпълним тази програма ще получим следния резултат

depth #1

depth #2

depth #3

depth #4

gcd of (15,123) 3



Локални променливи register

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

Очевидни кандидати за променливи register са индексите на масивите

for ( register int i = 0; i < sz; ++i )

ia[ i ] = i;

Формалните аргументи могат също да бъдат декларирани като променливи register

class iList

{

public

int value;

iList *next;

};

int find( register iList *ptr, int val )

{ // find val in ilinked list

while ( ptr )

{

if ( ptr->value == val ) return 1;

ptr = ptr->next;

}

return 0;

}

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


Дипломен проект на тема "Проектиране на обучаваща система в Web среда" по дисциплината "Програмни езици и системи"
Разработена е от с-на к-т Иван Маринов Калчев, под ръководството на к-н Дойчинов.
За нейната разработка е използван офис пакета на Microsoft – Office 2000.
В обучаващата система е представен програмния език C++. Тя има за цел бързото и лесно запознаване със синтаксиса на този програмен език, като той е описан в осем глави.




/ Трябва да сте регистриран за да напишете коментар /