Глава 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++ == `�ґ ) 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, worldn" );
получава като аргумент единствен низ. Обаче,
printf( "hello, %sn", 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. Изпращане на аргументи
За функциите се записва информация в една структура, наречена програмен стек от времето на изпълнение. Тази информация остава в стека докато функцията е активна. След като функцията приключи изпълнението си тази информация се изтрива автоматично. Цялата област, отделена за информацията, се нарича запис на активиране.
Списъкът от аргументите на една функция описва формалните й аргументи. Всеки формален аргумент се записва в записа на активиране. Размерът на този запис се определя от типовите спецификатори на аргументите. Изразите, записани в кръглите скоби при обръщението към функция се наричат фактически аргументи на обръщението. Изпращането на аргументи е процес на инициализиране на информацията за формалните аргументи чрез фактическите аргументи.
Подразбиращият се в С++ метод за инициализация при изпращането на аргументи е чрез копиране на стойностите за четене (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 ) &