Начало Решаване на проблеми



страница7/19
Дата20.01.2017
Размер4.54 Mb.
#13105
ТипГлава
1   2   3   4   5   6   7   8   9   10   ...   19
Раздел 3.7 (стр. 119) разглежда типа указател и

подаването-на-аргумент-чрез-указател по-подробно.


Упражнение 2-13. Променете декларацията на occurs от

списъка на аргументите на min(), така че да не бъде от тип

указател и изпълнете програмата отново.

2.14. Операторът switch


Няколко вложени if-else оператори често може да бъдат

синтактично правилни и въпреки това да не изразяват правилно

намеренията на програмиста. Например, едно свързване на if със

else клаузата, което противоречи на намеренията на

програмиста, често може да мине незабелязано. Някои промени на

оператори също могат да се окажат източници на грешки. С++

предлага един алтернативен метод за избор между множество от

взаимноизключващи се възможности. Това е оператора switch.

Например, да предполажим, че ни се налага да преброим

броя на появите на всяка от петте гласни в случайно избран

откъс от текст. (Традиционното мнение е, че "е" е най-често

срещаната гласна). Нашата програма ще има следния алгоритъм:


- чете последователно всеки символ дотогава, докато

няма повече символи за четене;


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

на тази книга. Ето резултатите от изпълнението й, които

потвърждават традиционното мнение относно честотата на поява

на гласната "е":


aCnt: 394

eCnt: 721

iCnt: 461

oCnt: 349

uCnt: 186
Програмата е реализирана като оператор swith с пет

разклонения за всяка гласна. В оператора switch различните

условия се отбелязват чрез етикета case. Изброяването на

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

програмата. Ето и самата програма:

ЇЇЇЇЇЇЇЇЇЇЇЇ


ЇЇЇЇЇЇЇЇЇЇЇЇ

93.
#include
enum Vowels { a = 'a', e = 'e', i = 'i',

o = 'o', u = 'u' };

main()

{

char ch;



int aCnt=0, eCnt=0, iCnt=0, oCnt=0, uCnt=0;
while ( cin >> ch )

switch ( ch )

{

case a:


++aCnt;

break;


case e:

++eCnt;


break;

case i:


++iCnt;

break;


case o:

++oCnt;


break;

case u:


++uCnt;

break;


}; // end switch(ch)
cout << "aCnt: \t" << aCnt << "\n";

cout << "eCnt: \t" << eCnt << "\n";

cout << "iCnt: \t" << iCnt << "\n";

cout << "oCnt: \t" << oCnt << "\n";

cout << "uCnt: \t" << uCnt << "\n";

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

програмата. Например, как би обработила програмата следните

входни данни?


UNIX
Главните букви U и I не могат да бъдат разпознати като гласни.

Нашата програма не е в състояние да брои гласните, които са

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

обаче, нека разгредаме по-внимателно оператора switch.

Стойността, записана след ключовата дума case, която

се нарича case-етикет, трябва да завършва с двуеточие. Всеки

case-етикет трябва да съдържа израз от тип integer. Два

case-етикета не могат да имат една и съща стойност; иначе по

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

Когато се изпълнява оператора switch, тестовият му

ЇЇЇЇЇЇЇЇЇЇЇЇ

ЇЇЇЇЇЇЇЇЇЇЇЇ

94.
израз се сравнява всеки от case-етикетите последователно. Ако

не може да бъде намерено съответствие между израза и

множеството от еникети не се изпълнява нищо.

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

изпълнени само операторите, записани след съответния

case-етикет. По-скоро може да се каже, че изпълнението започва

там, продължава през тялото и приключва в края на оператора

switch. Нека разгледаме по-подробно тази друга обща причина за

програмни грешки. Ето нашия предишен оператор switch, малко

променен:


switch ( ch )

{

case a: ++aCnt;



case e: ++eCnt;

case i: ++iCnt;

case o: ++oCnt;

case u: ++uCnt;

}; // end switch(ch)
cout << ch << "\n"; // for illustration
Ако ch има стойност 1 изпълнението започва от етикета i:. ICnt

се увеличава. Изпълнението, обаче, не спира до тук, а

продължава през границата на следващия case до затваряшата

скоба на оператора switch. По такъв начин се увеличават и

oCnt, и uCnt. Ако следващата стойност на ch е "e", то ще бъдат

увеличени eCnt, iCnt, oCnt и uCnt.

Програмистът явно трябва да укаже на компилатора да

прекрати изпълнението на операторите от тялото на оператора

switch. Обикновено, последният оператор след един case-етикет

е break.


Когато бъде срешнат оператора break се прекратява

изпълнението на оператора switch. Управлението се предава на

операторите, непосредствено следващи затварящата скоба на

switch. В нашия пример този оператор е:


cout << ch << "\n"; // for illustration
Към етикет, за който съзнателно е пропуснат оператора break, в

повечето случаи непремено трябва да бъде добавен коментар, че

това пропускане е обмислено. Ако от контекста, обаче, става

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

Кога е възможно програмистът да иска да пропусне

оператора break от case-етикета? Една възможност има, когато

дадено множество от стойности трябва да бъде обработено по

един и същ начин. Всеки елемент от това множество трябва да

бъде представен със собствен case-етикет. Например, да

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

записани с главни букви - ето правилната реализация:

ЇЇЇЇЇЇЇЇЇЇЇЇ


ЇЇЇЇЇЇЇЇЇЇЇЇ

95.
swithc (ch)

{

case A:



case a:

++aCnt;


break;

// ...


case U:

case u:


++uCnt;

break;


};

Операторът switch предлага еквивалент на безусловната

else клауза. Това е етикета default. Ако никой от етикетите не

съответствува на израза-тест и е зададен етикет default ще

бъдат изпълнени операторите след него. В нашата програма бихме

могли да изчисляваме броя на съгласните:

#include ( ch )

{
// ...

switch (ch)

{

case A:



case a:

++aCnt;


break;

// ...


case U:

case u:


++uCnt;

break;


default:

if isalpha( ch )

++conCnt;

break;


};

isalpha() е функция, записана в С блиблиотека; тя връща

стойност истина ако аргументът й е буква от азбуката. За да я

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

файл ctype.h.

Въпреки, че не е задължително добавянето на оператора

break след последния етикет, препоръчва се написването му. Ако

в последствие бъде написан допълнителен case-етикет в края на

оператора switch, липсата на break може внезапно да се окаже

съществена.

ЇЇЇЇЇЇЇЇЇЇЇЇ

ЇЇЇЇЇЇЇЇЇЇЇЇ

96.
Упражнение 2-14. Модифицирайте програмата така, че да

бъдат броени и прочетените интервали.


Упражнение 2-15. Изменете програмата така, че да бъдат

броени следните двубуквени последователности: ff, f1 и fi.

Итеративни оператори
Много програмни действия изискват прилагането на

определен брой оператори върху съвкупност от подобни обекти.

Една банкова програма може да бъде написана за да обработва

чекове. За всеки чек програмата трябва:


- да чете съдържанието му,
- да провери дали той има правилен счетоваден номер,
- да провери дали наличната сума е достатъчна да

покрие чековата стойност,


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

повтаряна за всеки чек. Естествено, езикът предлага оператори

за управление на итерации, наречени цикли, които позволяват на

програмата да описва многократно изпълнение на определен набор

от оператори.

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

определено условие остава истина. Банковата порграма вероятно

ще си дефинира следното условие:


while there exists a check to be processed

докато има чекове за обработване


Изпълнението на цикъла ще приключи, когато бъде обработен

последния чек; т.е. когато дефинираното условие получи

стойност лъжа. Това условие понякога се нарича управляващо

цикъла.


С++ поддържа три конструкции за цикъл: операторите

while, do и for. Основната разлика между тях е в метода за

управление на цикъла. И за трите оператора истинно условие е

всяка ненулева стойност. Нулевата стойност е еквивалентна на

невярно управляващо условие.

ЇЇЇЇЇЇЇЇЇЇЇЇ


ЇЇЇЇЇЇЇЇЇЇЇЇ

97.
2.15. Операторът while
Цикълът while има следния синтаксис:
while ( expression )

statement;


Операторът statement (или съставният оператор) се изпълнява до

тогава, докато условието expression има стойност истина.

Прилага се следната последователност от действия:
1. Изчислява се израза expression.
2. Изпълнява се оператора statement ако условието е

истина.
Ако при първото изчисляване на израза тест той има стойност

лъжа, оператора statement никога няма да бъде изпълнен.

Например, алгоритъмът за броене на гласни, описан в предишния

раздел за оператора switch, изисква символте да бъдат четени

от входния поток по един по един докато бъде достигнат края на

файла. Цикълът while е идеалния кандидат за реализиране на

това:
char ch;

while ( cin >> ch )

switch ( ch )

{ ...
Понеже switch се счита за един оператор, не е необходимо той

да бъде поставян в съставен оператор.

Цикълът while е особено подходящ за обработка на

низове и други указателни типове. Например,


ff( const char *st )

{

int len = 0;



const char *tp = st;
// compute length of st

while ( *tp++ ) ++len;


// now copy st

char *s = new char[ len + 1 ];

while ( *s++ = *st++ )

; // null statement


// ... rest of function

}
Упражнение 2-16. Напишете функция, която определя дали два

низа са еквивалентни.

ЇЇЇЇЇЇЇЇЇЇЇЇ


ЇЇЇЇЇЇЇЇЇЇЇЇ

98.
Упражнение 2-17. Напишете функция, която връща броя на

появите на даден символ в даден низ.


Упражнение 2-18. Напишете функция, която определя дали

даден подниз е част от даден низ.

2.16. Операторът for
Цикълът for най-общо казано се използува за постъпков

преглед на даннови структури с фиксирана дължина, като масиви

например. Ние вече сме го използували многократно.

Синтаксисът на оператора for има вида:


for ( init-statement; expression-1; expression-2 )

statement;


init-statement може да бъде декларация или израз. Най-общо той

се използува за инициализиране или присвояване на стойност на

променлива или множество от променливи. Може да бъде и празен

оператор. Ето няколко примера за правилни init-statement

оператори:
for ( int i = 0; ...

for ( ; /* null init-statement */ ...

for ( i = 0; ...

for ( int lo = 0, hi = max, mid = max/2; ...

for ( char *ptr = getStr(); ...

for ( i =0, ptr = buf, dbl = 0.0; ...


expression-1 служи за управление на цикъла. За толкова

операции, за колкото expression-1 получава стойност истина, се

изпълнява оператора statement. statement може да бъде единичен

или съставен оператор. Ако при първато изчисляване на

expression-1 той има стойност лъжа операторът statement никога

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

този вид:
(...; index < arraySize; ... )

(...; ptr; ... )

(...; *st1++ = *st2++; ... )

(...; ch = getNextChar(); ... )


expression-2 се изчислява след всека итерация на

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

променливите, инициализирани чрез init-statement. Ако при

първто изчисляване на expression-1 той има стойност лъжа,

expression-2 никога не се изчислява. Следват няколко примера

за правилни изрази от този вид:

ЇЇЇЇЇЇЇЇЇЇЇЇ

ЇЇЇЇЇЇЇЇЇЇЇЇ

99.
( ...; ...; ++i )

( ...; ...; ptr = ptr->next )

( ...; ...; ++i, --j, ++cnt )

( ...; ...; ) // null instance


Даден е следния цикъл for:
const int sz = 24;

int ia[ sz ];


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

ia[ i ] = i;


който се изпълнява по следния начин:
1. Пресмята се init-statement еднократно в началото на

цикъла. В този случай се дефинира променливата i и

се инициализира със стойност 0.
2. Изчислява се expression-1. Ако условието има

стойност истина, т.е. произволна ненулева стойност,

се изпълнява оператора statement. Ако в началото

условието има стойност лъжа операторът statement

никога не се изпълнява.
В този пример i се сравнява с sz. Докато i е

по-малко от sz се изпълнява опрератора:


ia[ i ] = i;
3. Изчислява се expression-2. Обикновено се променя(т)

променливата(те), инициализирани от init-statement.

В този пример i се увеличава с 1.
Така разгледахме една пълна итерация на цикъла for.

Сега се повтаря стъпка 2. Този процес може да бъде моделиран

със следния еквивалентен цикъл while:
init-statement;

while ( expression-1 )

{

statement;



expression-2;

}
Упражнение 2-19. Напишете функция, която сравнява два

масива за равенство.
Упражнение 2-20. Напишете функция, която търси дадена

стойност в масив. Ако такава стойност бъде намерена, да връща

индекса й в масива. Какво трябва да прави функцията ако

стойността не е намерена?

ЇЇЇЇЇЇЇЇЇЇЇЇ

ЇЇЇЇЇЇЇЇЇЇЇЇ

100.
2.17. Операторът do
Представете си, че сте били помолени да напишете една

интерактивна програма, която превръща мили в километри.

Програмата би могла да изглежда така:
int more = 1; // dummy value to start loop
while ( more )

{

val = getValue();



val = convertValue(val);

printValue(val);

more = doMore();

}
Проблемът тук е, че изразът, свързан с управлението на цикъла

е поставен в тялото му. При циклите while и for, обаче,

тялото на цикъла не се изпълнява никога, освен ако

управляващият израз няма стойност истина. Това означава, че

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

на изпълнението на цикъла. Цикълът do гарантира, че тялото му

винаги ще бъде изпълнено поне веднъж. Не е необходимо това да

бъде осъществявано принудтелно.

Цикълът do има следния синтаксис:


do

statement;

while ( expression );
Операторът statement се изпълнява преди пресмятането на израза

exprssion. Ако този израз има стойност лъжа изпълнението на

цикъла do приключва. Нашата програма сега изглежда така:
int more;
do {

val = getValue();

val = convertValue(val);

printValue(val);

more = doMore();

while ( more );

Оператори за скок
Тези оператори безусловно предават управлението на

програмата във вътрешността на дадена функция. Към тях се

причисляват операторите break, continue и goto. Следващият

раздел разглежда тези оператори последователно.

ЇЇЇЇЇЇЇЇЇЇЇЇ

ЇЇЇЇЇЇЇЇЇЇЇЇ

101.
2.18. Операторът break
Операторът break прекратява изпълнението на

най-вътрешния оператор while, do, for или switch. Изпълнението

продължава от оператора, записан непосредствено след

прекъснатия оператор. Например, нека на IntArray е необходима

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

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

й; иначе връща -1. Това се реализира по следния начин:
#include "IntArray.h"
IntArray::search( int Val )

{ // val in ia? return index; otherwise, -1

int loc = -1;

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

if ( val == ia[ i ]

{

loc = i;



break;

}
return loc;

}
Нама причина изпълнението на цикъла да продължава ако

стойността бъде намерена. Затова операторът break прекъсва

изпълнението на цикъла. Изпълнява се оператора return, записан

непосредствено след цикъла for.

2.19. Операторът continue
Операторът continue предизвиква прекратяване на

текущата итерация на най-вътрешния оператор while, do или for.

В случаите, когато използуваме циклите while и do,

изпълнението продължава с изчисляването на управляващия израз.

Когато използуваме цикъла for изпълнението продължава с

пресмятането на expression-2. За разлика от оператора break,

който прекратява изпълнението на цикъла, операторът continue

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

следващият програмен фрагмент чете от текстов файл по една

дума. Обработва се всяка дума, която започва с подчертаващо

тиренце; иначе се прекратява текущата итерация:
while ( cin >> inBuf )

{

if ( inBuf[0] != '_' )



continue; // terminate iteration

// ... process string ...

}
ЇЇЇЇЇЇЇЇЇЇЇЇ

ЇЇЇЇЇЇЇЇЇЇЇЇ

102.
2.10. Операторът goto
Ако операторите break и continue не съществуваха,

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

вън от циклите или от оператора switch. Операторът goto

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

в програмирането. Операторът goto осъществява безусловно

предаване на управлението в програмата. Операторът, на който

се предава управлението се отбелязва с етикет. Етикетът и goto

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

Операторът goto има следния синтаксис:
goto label;
където label е дефиниран от потребителя идентификатор. Един

етикет може да бъде използуван само за отбелязване на

оператора, на който се предава управлението, и трябва да

завършва с двуеточие.

Етикетът не може да бъде написан непосредствено преди

затваряща фигурна скоба. Един типичен метод за избягване на

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

Например,


end: ; // null statement

}
Операторът goto не може да прескача явна или неявна

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

дефиниция принадлежи на някакъв блок и се прескача целия блок.

Следващата програма, например, не е правилна:
#include "IntArray.h"

extern int getValue();

extern void processArray( IntArray& );
main()

{

int sz = getValue();



if ( sz <= 0 )

goto end; // illegal jump


IntArray myArray( sz );

procassArray( myArray );


end: ;

}
myArray представя един явен инициализатор - конструкторът на

класа IntArray.
Упражнение 2-21. Напишете отново main() така, че да

бъде правилна.

ЇЇЇЇЇЇЇЇЇЇЇЇ

ЇЇЇЇЇЇЇЇЇЇЇЇ

103.
Глава 3: Функции и обхват
За функциите може да се мисли като за потребителско

дефинирани операции. Най-общо казано функцията се представя

чрез име, а не чрез оператор. Операндите на функцията,

наречени нейни аргументи, се задават от затворен в скоби

списък от аргументи, разделени чрез запетаи. Резултатът на

функцията се нарича нейн тип за връщане. Функция, която не

връща стойност, има тип за връщане 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.

ЇЇЇЇЇЇЇЇЇЇЇЇ

ЇЇЇЇЇЇЇЇЇЇЇЇ

104.
#include

#include "localMath.h"


main()

{

int i, j;


// get value from standart input

cout << "Value: "; 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) да се поместват в заглавни файлове. В

последствие тези заглавни файлове могат да бъдат включвани

ЇЇЇЇЇЇЇЇЇЇЇЇ


ЇЇЇЇЇЇЇЇЇЇЇЇ

105.
навсякъде, където те са необходими. По този начин всички

файлове делят една обща декларация; ако тази декларация трябва

да бъде променена се коригира само един файл.

Заглавният файл може да бъде наречен 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


3.1. Рекурсия
Функция, която прави обръщение към себе си директно

или индиректно, се нарича рекурсивна. Функцията gcd(),

например, може да бъде написана отново като рекурсивна:
rgcd( int v1, int v2 )

{

if (v2 == 0 )



return v1;

return rgcd( v2, v1%v2 );

}
Една рекурсивна функция трябва винаги да дефинира

условие за спиране; иначе ще се получи зацикляне. В нашия

случай условието за спиране е нулев остатък.

Извикването:


rgcd( 15, 123 );
връща стойност 3. В таблица 3.1 са описани последователните

стъпки при изпълнението.

Последното извикване rgcd(3,0) удовлетворява условието

за спиране. Тя връща най-големия общ делител - 3. Тази

стойност става връщаната стойност на всички предишни

обръщения. Казва се, че тази стойност се промъква нагоре.

ЇЇЇЇЇЇЇЇЇЇЇЇ

ЇЇЇЇЇЇЇЇЇЇЇЇ

106.

ЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇ



v1 v2 return

ЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇ

15 123 rgcd(123, 15)

123 15 rgcd( 15, 3)

15 3 rgcd( 3, 0)

3 0 3


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

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

от нерекурсивния (или итеративния) й вариант, което се дължи

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

Кодът на функцията, обаче, вероятно е по-малък и не така

сложен.


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

на последователността от числа от 1 до числото. Например,

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

си рекурсивно решение.


unsigned long

factorial ( int val )

{

if ( val > 1 )



return val * factorial( val - 1);

return val;

}
В този случай условието за спиране е val да има стойност 1.
Упражнение 3-1. Напишете factorial() като итеративна

функция.
Упражнение 3-2. Какво ще се случи ако условието за

спиране има вида:
if ( val != 0 )
Упражнение 3-3. Как мислите, защо връщаната стойност

на функцията е дефинирана от тип unsigned long, докато

аргументът е от тип int?

ЇЇЇЇЇЇЇЇЇЇЇЇ


ЇЇЇЇЇЇЇЇЇЇЇЇ

107.
3.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 е само една препоръка за

ЇЇЇЇЇЇЇЇЇЇЇЇ


ЇЇЇЇЇЇЇЇЇЇЇЇ

108.
компилатора. Една рекурсивна функция, такава като gcd(),

например, не може напълно да разшири inline (въпреки, че

нейното първо повикване би могло да бъде разширено). Вероятно

функция с дължина 1200 реда няма да бъде разширена inline.

Изобщо механизмът inline е средство за оптимизиране на малки

няколкоредови често извиквани функции.

3.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, като

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

това е стесняващо конвертиране ще появи предупреждение.

Обръщението добива вида:

ЇЇЇЇЇЇЇЇЇЇЇЇ


ЇЇЇЇЇЇЇЇЇЇЇЇ

109.
gcd( 3, 6 );
и връща стойност 3.

С++ е строго типизиран език. По време на компилация се

прави проверка за съответствието на типовете както на списъка

от аргументи, така и на типа на резултата на извиканата

функция. Ако бъде открито несъответствие между фактическите

типове и типовете, декларирани в прототипа на функцията, ще

бъде приложено неявно конвертиране ако това е възможно. Ако не

е възможно неявно конвертиране или е неправилен броя на

аргументите ще се получи грешка по време на компилация.

Прототипът на функцята предлага на компилатора

информация за типовете, която му е необоходима при проверката

на типовете. Това е причината една функция да не може да бъде

викана преди да бъде декларирана.(ў)

3.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:

ЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇЇ

(ў)Тази способност за проверка на типовете се счита за особено

ценна, така че комисията ANSI за езика С е възприела прототипа

на функция от С++ за езика ANSI С.

ЇЇЇЇЇЇЇЇЇЇЇЇ

ЇЇЇЇЇЇЇЇЇЇЇЇ

110.
return;

return expression;


Операторът 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 either array is empty, quit

if ( scr == 0 || dst == 0 )

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.

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

добавя стойност за връщане във всяка точка на прекъсване на

функцията. Например,

ЇЇЇЇЇЇЇЇЇЇЇЇ

ЇЇЇЇЇЇЇЇЇЇЇЇ

111.
enum Boolean { FALSE, TRUE };
Boolean isEqual ( char *s1, char *s2 )

{ // if s1 == s2, return TRUE; else FALSE


// if either are null, not equla

if ( s1 == 0 || s2 == 0 )

return FALSE;

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

return TRUE;
while ( *s1 == *s2++ )

if (*s1++ == '\0' )

return TRUE;
// still here: not equal

return FALSE;

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

на програмата изисква да бъде връщано множество от стойности

програмистът може да направи едно от следните неща:
- Променлива, която е дефинирана вън от дадена функция

се нарича глобална. Достъп до една глобална

променлива има от вътрешността на всяка функция ако

тя е била подходящо декларирана. Програмистът може

да присвои втората стойност, която иска да връща

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

на тази стратегия е нейната простота. Недостатъкът й

е нейната неинтуитивност, което означава, че от

обръщението към функцията не става ясно, че на тази

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

може да бъде разбрано от другите програмисти, когато

правят промени в текста. Присвояването на стойност

на глобална променлива в някоя функция се нарича

страничен ефект.


- Може да бъде върнат събирателен тип данни, който

съдържа множество от стойности. За този тип

използуване класовете са по-гъвкави от масивите.

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

към масив; той може също да върне обект от тип клас,

указател или псевдоним на клас.


- Формалните аргументи магат да бъдат дефинирани като

указателни или съотнасящи типове. Те могат да бъдат

дефинирани така, че да съдържат самите стойности.

(Този метод се разглежда в раздел 3.6 (стр. 118)

по-нататък в тази глава).

ЇЇЇЇЇЇЇЇЇЇЇЇ


ЇЇЇЇЇЇЇЇЇЇЇЇ

112.
3.5. Списък от аргументи на функция
Различните функции в една програма могат да

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

разделен достъп до стойности). При първият начин се използуват

глобални променливи, а при втория списък от формални аргументи

на функция.

Общата достъпност на глобалните променливи за цялата

програма носи голяма полза, но изисква и осбено внимание.

Видимостта на глобалните променливи ги прави удобен метод за

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

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

при комуникацията между функциите :
- Функциите, които използуват глобалните променливи,

зависят от съществуването и типа им, което прави

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

глобалните променливи увеличават вероятността от

възникване на грешки. Освен това извършването на

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

програма.
- Ако една глобална променлива получи неправилна

стойност трябва да бъде прегледана цялата програма

за да бъде открита причината; няма никаква

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


- Много трудно се пишат правилни рекурсивни програми

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


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

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

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

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

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

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

Съответно, всяка функция сама по себе си може да бъде

използувана и в други програми; не е необходимо да бъде

свързана с конкретно приложение.

Пропускането на аргумент или изпращането на аргумент

от неправилен тип са източници на сериозни грешки по време на

изпълнение на програма, написана на предишния ANSI C език. Със

въвеждането на строгата проверка на типовете, тези интерфейсни

грешки почти винаги се откриват по време на компилация.

Вероятността за възникване на грешка при изпращане на

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

от аргументи - някои функции на FORTRAN приемат до 32

аргумента. Като едно общо правило можеда се приеме, че броят

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

функция се нуждае от повече арагументи то вероятно тя се

опитва да направи прекалено много неща; един по-добър проект

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

функции.

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

програмистът може да дефинира класов тип, който да съдържа

стойностите на аргументите. Полезността на това се изразява в

следното:

ЇЇЇЇЇЇЇЇЇЇЇЇ


ЇЇЇЇЇЇЇЇЇЇЇЇ

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

аргументи.


2. Проверките за валидност на стойностите за

аргументите могат да бъдат изпълнявани от

членовете-функции на класа, а не вътре във

функцията. Това намалява размера на функцията и я

прави по-лесна за разбиране.

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


Списъкът от аргументи на функция не може да бъде

пропускан. Функция, която не получава аргументи може да се

опише или с празен списък от аргументи или със списък,

съдържащ единствено ключовата дума void. Например, следните

две декларации на fork() са еквивалентни:
// equivalent declarations

int fork();

int fork( void );
Списъкът от аргументи се нарича сигнатура на функцията, защото

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

функцията от друг. Името и сигнатурата на една функция я

идентифицират по уникален начин. (Раздел 4.3 (стр. 152), който

се отнася за презарежането на функции обсъжда тази идея

по-подробно).

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

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

бъде записано име. Неправилно е съкратеното записване на

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

Например,
min( int v1, v2 ); // error

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


В сигнатурата не могат да фигурират два аргумента с едни и

същи имена. Имената позволяват на аргументите да бъдат

достъпни във тялото на функцията. Следователно имената на

аргументите не са необходими за декларацията на функцията. Ако

са записани, би следвало да служат за подпомагане на

документирането. Например,


print( int *array, int size );
Не съществуват езиково наложени ограниченя за задаване на

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

декларацията на една функция. Обаче, това може да обърака

четящия програмата.

ЇЇЇЇЇЇЇЇЇЇЇЇ

ЇЇЇЇЇЇЇЇЇЇЇЇ

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

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

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

сигнатурата на функцията.

Многоточието преустановява проверката на типовете.

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

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

следните две форми на запис:


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();
е правилно и за двете функции.

ЇЇЇЇЇЇЇЇЇЇЇЇ


ЇЇЇЇЇЇЇЇЇЇЇЇ

115.
Специалната сигнатура: инициализация по подразбиране
Стойността по подразбиране е стойност, която въпреки

че не е универсално приложима, може да се окаже подходяща за

мнозинството от случаите. Подразбиращите се стойности ни

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

потребителя на операционната система UNIX, например, всеки

текстов файл, създаден от него, се дефинира по подразбиране с

разрешение за четене и запис за него и само за четене - за

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

достъп до файла, като системата UNIX поддържа прост механизъм

за модифициране или заменяне на подразбиращите се стойности.

Дадена функция може да определя подразбиращи се

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

синтаксиса на сигнатурата. Функция, която създава и

инициализира двумерен символен масив за да симулира екран на

терминал може да зададе стойности по подразбиране за

височината, ширината и вида на основата за символите на

екрана:
char * screenInit( int height = 42, int width = 80,

ckar 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 е тази стойност, която най-вероятно ще бъде

задавана от потребителя.

Една функция може да дефинира подразбиращи се

инициализатори за всичките си аргументи или за някакво

ЇЇЇЇЇЇЇЇЇЇЇЇ

ЇЇЇЇЇЇЇЇЇЇЇЇ

116.
подмножество от тях. Най-десният инициализиран аргумент трябва

да бъде снабден с подразбиращ се инициализатор преди който и

да е аргумент от ляво на него да бъде снабден с такъв. Отново

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

обръщението към функция. Няма ограничение инициализаторът на

аргумент по подразбиране да бъде константен израз.

Инициализатор на аргумент по подразбиране може да се

дефинира само веднъж в даден файл. Например, неправилен е

следния запис:
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

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() за трети път:

ЇЇЇЇЇЇЇЇЇЇЇЇ


ЇЇЇЇЇЇЇЇЇЇЇЇ

117.
#include "ff.h"

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

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

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


За функциите се записва информция в една структура,

наречена програмен стек от времето на изпълнение. Тази

информация остава в стека дакато функцията е активна. След

като функцията приключи изпълнението си тази информация се

изтрива автоматично. Цялата област, отделена за информацията,

се нарича запис на активиране.

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

фолмалните й аргументи. Всеки формален аргумент се записва в

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

типовите спецификатори на аргументите. Изразите, записани в

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

фактически аргументи на обръщението. Изпращането на аргументи

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

аргументи чрез фактическите аргументи.

Подразбиращият се в С++ метод за инициализация при

изпращането на аргументи е чрез копиране на стойностите за

четене (rvalue) на фактическите аргументи в областта, отделена

за формалните аргументи. Това се нарича изпращане по стойност.

При изпращането по стойност функцията никога няма

достъп до фактическите аргументи на обръщението. Стойностите,

които функцията обработва са нейни собствени локални копия; те

са записани в стека. Изобщо, промените направени над тези

стойности не се отразяват на стойностите на фактическите

аргументи. Когато функцията приключи работата си и записа на

активирането бъде изтрит тези локални стойности се изгубват.

При изпращането по стойност съдържанието на

фактическите аргументи не се променя. Това означава, че

програмистът не е длъжен да запазва и възстановява стойностите

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

механизма за изпращане по стойност може да се предполага, че

всеки формален аргумент, който не е деклариран от тип const

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

функцията. Извикването по стойност има минимален потенциал за

нанасяне на щети и изисква минимум усилия от потребителя.

Изпращането по стойност е един разумен механизъм за изпращане

на аргументи по подразбиране.

Изпращането по стойност, обаче, не е подходящо за

всяка функция. Механизмът за изпращане по стойност не е

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

обект от тип клас. Времето и паметта, необходими за

копиране и разполагане на класовия обект в стека

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

приложения от реалната практика.
- Когато стойностите на аргументите трябва да бъдат

обработени. Функцията swap() е един пример, при

който потребителят иска да промени стойностите на

фактическите аргументи, но това не може да бъде

ЇЇЇЇЇЇЇЇЇЇЇЇ

ЇЇЇЇЇЇЇЇЇЇЇЇ

118.
направено чрез механизма за изпращане по стойност.
void swap( int v1, int v2)

{

int tmp = v2;



v2 = v1;

v1 = tmp;

}
swap() разменя локалните копия на аргументите си. Фактическите

променливи, изпратени на swap(), остават непроменени. Това се

илюстрира от следната програма, която вика swap():
#include

vid 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 );
Когато компилираме и изпълним тази програма, показаните

резултати вече ще бъдат правилни:

ЇЇЇЇЇЇЇЇЇЇЇЇ

ЇЇЇЇЇЇЇЇЇЇЇЇ

119.

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 са правилно разменени.

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

Този аргумент изпраща на функцията стойността за запис

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

се извършват над фактическите аргументи, а не над

локалните копия.
2. Не съществуват ограничения за изпращането на големи

класови обекти като аргументи на функция. Когато се

използува механизма за изпращане по стойност се

копира целия обект при всяко обръщение към

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

функцията се препоръчва да бъде деклариран като const.

Забележете, обаче, че е неправилно декларирането на х като

const в следния пример:

ЇЇЇЇЇЇЇЇЇЇЇЇ

ЇЇЇЇЇЇЇЇЇЇЇЇ

120.
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 съществуват осем различни цели типа като при седем от

тях винаги ще се получава несъотвествие на произволен

компактен псевдонимен аргумент. В случаите, когато не можем да

предвидим типа на фактическият аргумент не е безопасно да

разчитаме на семантиката на изпращане чрез псевдоним.

Аргументът-указател позволява модифицирането на

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

самия указател? Тогава ще декларираме указател-псевдоним:

ЇЇЇЇЇЇЇЇЇЇЇЇ

ЇЇЇЇЇЇЇЇЇЇЇЇ

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

{

int *tmp = v2;



v2 = v1;

v1 = tmp;

}
Декларцията
int *&p1;
трябва да бъде четена от ляво на дясно. p1 е псевдоним на

указател към обект от тип int. Променената реализация на

main() ще изглежда така:
#include

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, които трябва да

осигуряват възможност както за четене, така и за запис:

ЇЇЇЇЇЇЇЇЇЇЇЇ

ЇЇЇЇЇЇЇЇЇЇЇЇ

122.
int IntArray::operator[]( int index )

{

return ia[ index ];



}
Intarray myAarray[ 8 ];

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



Каталог: files -> tu files
tu files -> Увод в компютърната графика
tu files -> Xii. Защита и безопасност на ос
tu files -> Електрически апарати
tu files -> Средства за описание на синтаксиса
tu files -> Stratofortress
tu files -> Писане на скриптове за bash шел : версия 2
tu files -> 6Технологии на компютърната графика 1Модели на изображението
tu files -> Z=f(x), където x- входни данни; z
tu files -> Body name библиотека global Matrix imports (достъп по име) … var m[N, N] := … end decl., proc … resource f final code imports node, Matrix end name var x: node node; if x … Matrix m[3,4] :=: … end


Сподели с приятели:
1   2   3   4   5   6   7   8   9   10   ...   19




©obuch.info 2024
отнасят до администрацията

    Начална страница