Глава 3: Изрази и оператори
Съдържание на трета глава :

3.1. Какво представлява изразът?
3.2. Аритметични операции
3.3. Операции за равенство и отношение, логически операции
3.4. Оператор за присвояване
3.5. Оператори за увеличаване и намаляване с 1
3.6. Операторът sizeof
3.7. Аритметичен оператор if
3.8. Оператори за работа с битове
3.9. Приоритет
3.10. Преобразуване на типове
3.11. Оператори
3.12. Оператори за управление
3.14. Операторът switch
3.15. Oператорът while
3.16. Операторът for
3.17. Операторът do
3.18. Операторът break
3.19. Операторът continue
3.20. Операторът goto




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



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

3.1. Какво представлява изразът?

Всеки израз обединява една или повече операции. Обектите, над които се прилагат операциите се наричат операнди. Операциите се записват чрез опетори. Например, В С++ проверката за равенство се осъществява от оператора “= =”. Операторите, които имат един операнд, се наричат унарни, докато операторите с два аргумента са бинарни. Операндите на бинарните оператори се делят на леви и десни. Някои оператори представят както унарни, така и бинарни операции. Например,

*ptr

е един унарен указателен оператор. Той връща стойността, съхранена в адреса на обекта ptr. Обаче,

var1 * var2

представя бинарния оператор умножение. Той изчислява произведението на операндите си var1 и var2.

За изчисляването на един израз се изпълнява една или повече операции за да бъде даден резултат. Изключвайки случая, когато е указано обратното, резултатът, връщан от израза е стойност за четене (rvalue). Типът на резултата на един израз се определя от типа на операндите му. Когато операндите имат различен тип се извършва преобразуване на типовете съобразно предварително дефиниран набор от правила. Раздел 3.10 раазглежда подробно преобразуването на типовете.

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

Най-простата форма на израз се получава от единична литерална константа или променлива.

Т.е. “операнд” без операция. Резултатът е стойността за четене на операнда. Например,

3.14159

"melancholia" upperBound

Резултатът на 3.14159 е 3.14159. Типът му е double. Резултатът на “melancholia” e адреса в паметта на първия елемент на низа. Типът му е char*. Резултатът на upperBound е нейната стойност за четене (rvalue). Типът му се определя от дефиницията на променливата.

Следващите раздели разглеждат предварително дефинираните в С++ оператори, представени в удобен за изучаването им ред.



3.2. Аритметични операции



Операция
Функция
Използуване

Умножение
Expr * expr

/
деление
expr / expr

%
деление по модул
expr % expr

Събиране
Expr + expr

изваждане
Expr - expr



Таблица 3.1 Аритметични операции

Делението на цели числа дава цяло число. Ако частното има дробна част тя се отрязва. Например, и двата израза

21 / 6;

21 / 7;

дават като резултат 3.

Оператoрът за деление по модул (“%”) изчислява остатъка от делението на две стойности. Той може да се използува само с операнди от цял тип. Левият операнд на операторът деление по модул е делимото. Делителят е десният операнд на операцията. Двата операнда трябва да са от цял тип. Следват няколко примера на правилни и неправилни изрази, използуващи оператора за деление по модул:

3.14 % 3 // error: floating point operand

21 % 6 // ok: result is 3

21 % 7 // ok: result is 0

int i;

double f;

i % 2 // ok: non-zero result indicates i is odd

i % f // ok: floating point operand

В някои случаи изчисляването на аритметичен израз ще върне неправилен или недефиниран резултат. Това се отнася за аритметични изрази. Може да се дължи на самото естество на математическата операция - деление на нула, например, - или да зависи от компютъра - препълване. Например, в променлива от тип unsigned char могат да се записват стойности от 0 до 255. При следната операция за умножение на променливата от тип unsigned char се дава стойност 256.

unsigned char uc = 32;

int i = 8;

uc = i * uc; // overflow

За да се представи 256 са необходими 9 бита. Присвояването на 256 на uc предизвиква препълване на паметта, отделена за този тип данни. Истинската стойност, записана на мястото на uc, е недефинирана и ще бъде различна за различните машини.



3.3. Операции за равенство и отношение, логически операции

В следствие на изпълнението на тези операции се получава стойност истина или лъжа. Условието, което има стойност истина, връща 1, а това с лъжа - 0.

Логическата операция AND (“&&”) връща стойност истина само ако и двата й операнда се остойностяват като истина. Логическата операция OR (“||”) връща стойност истина когато един от двата й операнда имат стойност истина. Операндите се изчисляват от ляво на дясно. Изчислението се прекратява когато стойността на израза може да бъде определена. В изразите:

expr1 && expr2

expr1 || expr2

expr2 не се изчислява съответно:

при логическата операция AND, ако expr1 има стойност лъжа; при логическата операция OR, ако expr1 има стойност истина.

операция
функция
използуване

!
логическо не
!expr1

<
по-малко
expr1 < expr2

<=
по-малко или равно
expr1 <= expr2

>
по-голямо
expr1 > expr2

>=
по-голямо или равно
expr1 >= expr2

==
равенство
expr1 == expr2

!=
различно
expr1 != expr2

&&
логическо и
expr1 && expr2

||
логическо или
expr1 || expr2


Таблица 3.2. Операции за равенство и отношение, логически операции

Ползата от такова изчисление на операцията AND се вижда, когато expr1 задава такова условие в израза, което ако има стойност лъжа, може да направи пресмятането на expr2 опасно. Например,

while ( ptr != 0 && ptr ->

value < upperBound && notFound( ia[ ptr -> value ] ))

Указател със стойност 0 не адресира никакъв обект. Прилагането на оператора за избор на елемент към указател със стойност 0 причинява винаги неприятности. Първият операнд на израза с AND предотвратява тази възможност. Излизането на индекса извън границите на масива е също така неприятно. Вторият операнд е предвиден да предотвратява тази възможност. Когато първите два операнда върнат стойност истина третият операнд може спокойно да бъде изчислен.

Логическият оператор NOT ("!") получава стойност истина когато неговият операнд има стойност 0; иначе има стойност лъжа. Например:

int found = 0;

while ( !found )

{

found = lookup( *ptr++ );

if ( ptr == endptr ) // at end

return 0; }

Изразът ( !found ) има стойност истина докато found има стойност 0.

Използуването на оператора NOT се свързва с въпроса за стила на програмиране. Например, значението на израза ( !found ) е ясно: не е намерен. Така ясно ли е, обаче, какво означава следното условие?

!strcmp( string1, string2 )

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

!strcmp( string1, string2 )

означава следното: ако string1 не е равен на string2. В този случай използуването на оператора NOT би могло да доведе до неясноста на условието.



3.4. Оператор за присвояване

Левият операнд на оператора за присвояване (“=”) трябва да бъде стойност за запис (lvalue). Резултатът от изпълнението на този оператор е записването на нова стойност в паметта, отделена на операнда. Например, нека са дадени следните три дефиниции:

int i;

int *ip;

int ia[ 4 ]; за които са валидни следните оператори:

ip * &i;

i = ia[ 0 ] + 1;

ia[ *ip ] = 1024;

*ip = i * 2 + ia[ i ];

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

Предвидено е операторите за присвояване да могат да бъдат обединявани като всеки от операндите да бъде присвояван на същия тип данни. Например,

main()

{

int i, j;

i = j = 0; // ok: each assigned 0

// ... }

на i и j се дава стойност 0. Редът за пресмятане е от ляво на дясно.

Обединяването на операторите за присвояване позволява един по-компактен запис, като е при написания отново конструктор на IntArray, например ( вж. Раздел 2.8 за неговата оригинална реализация):

IntArray::IntArray( int sz )

{

ia = new int[ size = sz ];

// ... }

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

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

int arraySum( int ia[], int sz )

{

int sum = 0;

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

usm += ia[ i ];

return sum; }

Обобщената синтактична форма на смесения оператор за присвояване има вида:

a op = b;

където op може да бъде един от следните десет оператора:

+=, -=, *=, /=, %=, <<=, >>=, &=, ^= , |=.

Всеки смесен оператор е еквивалентен на следното:

a = a op b;

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

sum = sum + ia[ i ];

Упражнение 3-1. Следният запис е правилен. Защо? Как бихте могли да го промените?

main()

{ int i = j = k = 0; }

Упражнение 3-2. Следният запис също е правилен. Защо? Как бихте могли да го промените?

main()

{

int i, j;

int *ip;

i = j = 0; }



3.5. Оператори за увеличаване и намаляване с 1

Операторите за увеличаване (“++”) и намаляване (“—“) с единица предлагат един удобен, компактен запис за добавяне и изваждане на 1 от променлива. За двата оператора може да се мисли като за оператори за присвояване; операндите трябва да бъдат стойности за запис (lvalue). Всеки оператор има както префксна, така и постфиксна форма. Например,

main()

{

int c; ++c; // prefix increment

c++; // postfix increment }

Нека да илюстрираме използването на пост- и префиксната форма на операторите като дефинираме стеков клас IntStack, който поддържа цели стойности. За IntStack са дефинирани две операции:

1. push(int v), която поставя стойността на v на върха на стека.

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

1. препълване: опит за запис в стека, когато е пълен.

2. изпразване: опит за четене от стека, когато той е празен.

IntStack ще бъде представен чрез един масив от цели числа. Следователно, необходими са следните членове (елементи):

int size;

int *ia;

int top;

top винаги ще съдържа индекса на елемента на върха на стека. Това означава, че за празен стек top има стойност -1. Стекът е пълен когато top има стойност size-1.

Написването на функциите isEmpty() и isFull(), които връщат стойности истина или лъжа е елементарно:

typedef int Boolean;

extern const int BOS; // Bottom of Stack

Boolean IntStack::isEmpty()

{ return (top == BOS);}

Boolean IntStack::isFull()

{ return (top == size-1);}

Реализацията на push() илюстрира префиксната форма на оператора за увеличение с 1. Да припомним, че top сочи текущия връх на стека. Новият елемент трябва да бъде поставен в елемент с едно по-голям от текущата стойност на top:

void IntStack::push( int v )

{ // add v to the top of stack

if ( isFull() ) grow(); // enlarge stack

ia[ ++top ] = v; }

Префиксната форма на ++ увеличава стойността на top преди тази стойност да бъде използувана като индекс в ia. Това е компактния запис на следните два оператора:

top = top + 1;

ia[ top ] = v;

grow() увеличава размера на стека с някакъв предварително зададен брой елементи. Раздел 5.1 съдържа реализацията на grow().

Реализирането на pop() илюстрира постфиксната форма на оператора за намаляване с 1. Припомняме отново, че top сочи текущия връх на стека. След като бъде извлечен елемент от стека, top трябва да бъде намален с 1.

int IntStack::pop()

{ // return the topmost element of stack

if( isEmpty() ) // report error and exit() ;

return ia[ top-- ]; }

Постфиксната форма на—намалява стойността на top след като тази стойност е била използувана като индекс на ia. Това е компактен запис на следната двойка оператори:

ia[ top ];

top = top - 1;

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

Това,което остава за цялостното дефиниране на IntStack, е управлението на стека му. Понеже вътрешното представяне на нашия стеков клас е същото както и на дефинирания по-рано клас IntArray, ние ще използуваме реализацията на IntArray като базова за IntStack (вж. Раздел 2.8 ) за дефиницията на класа IntArray):

#include "IntArray.h"

typedef int Boolean;

const int BOS = -1; // Bottom of Stack

class IntStack :

private IntArray

public: IntStack( int sz = ARRAY_SIZE ) : IntArray( sz ),

top( BOS ) {} Boolean isEmpty();

Boolean isFull();

void push( int );

int pop();

void grow() {}

protected: int top;

}

При описанието на IntArrayRC, IntArray беше деклариран като базов клас public. При описанието на IntStack, IntArray е деклариран като базов клас private. Първата разлика между базовите класове private и public е, че на базов клас private не могат да бъдат присвоявани обекти от произлезлия клас. Например,

extern swap( IntArray&, int, int );

IntArray ia; IntArrayRC ia1;

IntStack is; ia = ia1; // ok: public base class

ia = is; // error: private base class

swap( ia1, 4, 7 ); // ok: public base class

swap( is, 4, 7 ); // error: private base class

Втората разлика между базовите класове private и public е, че наследените членове на базовия клас private се разглеждат като собствени членове на произлезлия клас. Това означава, че интерфейсът public на класа IntArray не е достъпен през класовите обекти на IntStack. Например,

int bottom = is[0]; // error: operator[] private

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

1. Като метод за създаване на подтип на съществуващ клас, както е при дефинирането на IntArrayRC, произлязъл от класа IntArray, описан в глава 1.

2. Като метод за повторно използуване на дадена реализация с цел създаване на нов тип клас, както е при дефинирането на IntStack, произлязал от класа IntArray.

IntStack не е подтип на IntArray. Той не разделя операциите, които могат да бъдат прилагани към обектите на класа IntArray. Чрез определянето на IntArray като базов клас private на IntStack не се допуска случайното прилагане на операциите на IntArray върху обекти на класа IntStack. Освен това, ако един обект от класа IntStack случайно бъде присвоен на обект от класа IntArray целостта на стека може сериозно да бъде повредена. Предотвратяването на такива инциденти е втората причина за дефинирането на IntArray като базов клас private на IntStack. Един базов клас private не може да бъде присвояван на класов обект от произлезлия му клас. Ето един пример за обекти от клас IntStack:

#include <stream.h>

#include "IntStack.h"

IntStack myStaack;

const DEPTH = 7;

main()

{

for ( int i = 0; i < DEPTH; ++i ) myStack.push(i);

for ( i = 0; i < DEPTH; ++i ) cout << myStack.pop() << "\t";

cout << "\n"; }

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

6 5 4 3 2 1 0

Упражнение 3-3. Как мислите, защо С++ не е наречен ++С?



3.6. Операторът sizeof

Операторът sizeof връща размера в байтове на израз или типов спецификатор. Той може да бъде използуван в една от следните две форми:

sizeof (type-specifier);

sizeof expr;

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

int ia[] = { 0, 1, 2 };

const sz = sizeof ( ia ) / sizeof ( int );

Следващата програма илюстрира използуването на оператора sizeof за голямо разнообразие от типови спецификатори.

#include <stream.h>

#include "IntStack.h"

main()

{

cout << "int :\t\t" << sizeof( int );

cout << "\nint* :\t\t" << sizeof( int* );

cout << "\nint& :\t\t" << sizeof( int& );

cout << "\nint[3] :\t" << sizeof( int[3] );

cout << "\n\n"; // ot separate output

cout << "Intstack :\t" << sizeof( IntStack );

cout << "\nIntstack* :\t" << sizeof( IntStack* );

cout << "\nIntstack& :\t" << sizeof( IntStack& );

cout << "\nIntstack[3] :\t" << sizeof(IntStack[3]);

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

int : 4

int* : 4

int& : 4

int[3] : 12

IntStack : 12

IntStack* : 4

IntStack& : 4

IntStack[3] : 36



3.7. Аритметичен оператор if

Аритметичният оператор if е един триместен (тернарен) оператор, който има следния синтаксис:

expr1 ? expr2 : expr3;

expr1 винаги се изчислява. Ако стойността му е истина - т.е. някаква ненулева стойност - се изчислява expr2; иначе - expr3. Следната програма илюстрира как може да бъде използуван аритметичния оператор if.

#include <stream.h>

main()

{

int i = 10, j = 20, k = 30;

cout << "\nThe Larger value of " << i << " and " << j << " is " << ( i > j ? i : j );

cout << "\nThe value of " << i << " is" << ( i % 2 ? " " : " not " ) << "odd";

// the arithmetic if can be nested, bit

// too deep a nesting will be difficult to read

// max is set to the largest of 3 variables

int max = ( ( i>j )

? ( ( i>k ) ? i : k )

: ( ( j>k ) ? j : k ) );

cout << "\nThe larger value of " << i << ", " << j << " and " << k << " is " << max << "\n"; }

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

The larger value of 10 and 20 is 20

The value of 10 is not odd

The larger value of 10, 20 and 30 is 30



3.8. Оператори за работа с битове

Тези оператори разглеждат операндите си като подредена съвкупност от битове. Всеки бит може да има стойност 0 (off) или 1 (on). Операторите за работа с битове позволяват на програмиста да проверява и инициализира индивидуални битове или битови подмножества.

Операндите на тези оператори трябва да бъдат от тип integer. Въпреки че те могат да бъдат със или без знак препоръчва се използването на операнди без знак. Как точно се обработва знаковия бит при тези оператори зависи от реализацията им; програми, които работят с дадена реализация може да не работят с друга. По такъв начин използуването на операнд без знак подпомага осигуряването на мобилност на програмата.

Първо ние ще обясним как работи всеки оператор. После ще дадем примери за използуване на всеки от операторите за работа с битове. В раздел 6.4 ще реализираме класа BitVector.

операция
функция
използуване

~
поразредно логическо допълване до 1
~expr1

<<
изместване в ляво
expr1 << expr2

>>
изместване в дясно
expr1 >> expr2

&
поразредно логическо И
expr1 & expr2

|
поразредно логическо ИЛИ
expr1 | expr2


Таблица 3.3. Оператори за работа с битове

Операторът (“~”) обръща битовете на операнда си. Всеки бит със стойност 1 става 0 и всеки нулев бит получава стойност 1.

unsigned char bits = 0227;

1 0 0 1 0 1 1 1

bits = ~bits;

0 1 1 0 1 0 0 0

Операторите (“<< , >>”) изместват битовете на левия операнд с някакъв брой позиции в ляво или в дясно.

unsigned char bits = 1;

0 0 0 0 0 0 0 1

bits = bits << 1;

0 0 0 0 0 0 1 0

bits = bits << 2;

0 0 0 0 1 0 0 0

bits = bits >> 3;

0 0 0 0 0 0 0 1

Излишните битове на операндите се отстраняват. Операторът за изместване в ляво (“<<”) вмъква битове със стойност 0 от дясно. Операторът за изместване в дясно (“>>”) вмъква битове 76. със стойност 0 от ляво. Ако операндът има знак то може да се добавят копия на знаковия бит или нулеви битове; това зависи от машината.

Операторът AND ("&") работи с два операнда. Резултатът за всяка битова позиция е 1 ако двата операнда съдържат битове със стойност 1; иначе резултатът е нулев бит. (Този оператор не трябва да бъде бъркан с логическия оператор AND ("&&").

unsigned char result:

unsigned char b1 = 0145;

0 1 1 0 0 1 0 1

unsigned char b2 = 0257;

1 0 1 0 1 1 1 1

result = b1 & b2;

0 0 1 0 0 1 0 1

Операторът XOR (изключващо или) (“^”) работи с два операнда. Резултатът за всяка битова позиция е 1 ако един от двата, но не и двата операнда съдържат битове със стойност 1; иначе резултатът е нулев бит.

result = b1 ^ b2;

1 1 0 0 1 0 1 0

Операторът OR (“|”) работи с два операнда. Резултатът за всяка битова позиция е 1 ако един от двата или двата операнда съдържат битове със стойност 1; иначе резултатът е нулев бит.

result = b1 | b2;

1 1 1 0 1 1 1 1

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

Ето един пример. Нека учител има 30 студента в даден клас.

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

unsigned int quiz1 = 0; Учителят трябва да може да записва във всеки бит 0 или 1, както и да проверява съдържанието му. Например, студентът с номер 27 си е взел изпита и преминава. Учителят трябва да запише 1 в съответния бит. Първата стъпка е да запише в 27-я бит 1 като останалите битове запазят стойността си. Това може да бъде направено чрез оператора за изместване в ляво (“<<”) и цялата константа 1.

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1

0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 << 27

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

Ако тази стойност се използува като операнд за OR заедно с quiz1 всички битове освен 27-я ще останат непроменени. А 27-я ще получи стойност 1:

quiz1 |= 1<<27;

Представете си, че учителят повтори изпитването и установи, че студент 27 не се справя задоволително. Той трябва да може да запише отново 0 в бит 27. Този път цялото число трябва да има във всички битове стойност 1 с изключение на 27-я. Забележете, че това число е инверсно на числото от предния пример. Така, че може да бъде използуван оператора NOT:

1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 ~(1 << 27)

1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

Ако тази стойност се използува като операнд за AND заедно с quiz1 всички битове освен 27-я ще останат непроменени. А 27-я ще получи стойност 0:

quiz1 &= ~(1 << 27);

Ето как учителят би могъл да провери стойността на различните битове. Да разгледаме пак студент 27. Първата стъпка е да се запише в 27-я бит на едно цяло число 1. Като се приложи оператора AND над това число и quiz1 ще получим истина ако бит 27 на quiz е също 1; иначе ще бъде върната 0 (лъжа).

int hasPassed = quiz1 & (1 << 27);

Упражнение 3-4. Дадени са следните две дефиниции:

unsigned int ui1 = 3;

i2 = 7; Какъв е резултата на изразите:

(a) ui1 & ui2 (c) ui1 | ui2

(b) ui1 && ui2 (d) ui1 || ui2

Упражнение 3-5. Какво означава присвояването на 0377 на променлива от тип unsigned char в термините на битовия й шаблон? Начертайте картинката.

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

Упражнение 3-8. Съществува метод за получаване на степените на двойката като се използува оператора за изместване в ляво и константата 1. Генерирайте таблица от първите 16 стойности.



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

6 + 3 * 4 / 2 + 2 Едно изчисление от ляво на дясно дава резултат 20. Други възможни резултати са 9, 14 и 36. Кой е правилния? 14.

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

1. 3 * 4 => 12 2. 12 / 2 => 6 3. 6 + 6 => 12 4. 12 + 2 => 14

Ето един смесен израз, в който има коварна грешка. Проблемът се състои в това, че операторът за неравенство ("!=") има по-висок приоритет от оператора за присвояване:

while ( ch = nextChar() != ‘\0’ )

Намерението на програмиста е да присвои на ch следващия символ и тогава да провери дали той не е ‘\0’. Обаче, фактически се проверява дали следващият символ е ‘\0’. След това на ch се присвоява стойност истина или лъжа като резултат от проверката. Никога на ch не са присвоява следващия символ. Може да бъде зададен друг ред на изпълнение на операциите, като се обособят подизрази чрез използуване на скоби. При изчисляване на смесени изрази първо се пресмятат всички затворени в скоби подизрази. Всеки подизраз се замества от резултата му; изчислението продължава. Най-вътрешните скоби се изчисляват преди по-външните. Например,

4 * 5 + 7 * 2 ==> 34

4 * ( 5 + 7 * 2 ) ==> 76

4 * ( ( 5 + 7 ) * 2 ) ==> 96 Ето и по-горе споменатия смесен израз, в който са добавени скоби съобразно намерението на програмиста:

while ( (ch = nextChar()) != ‘\0’ )

Таблица 3.4. представя пълния набор от С++ опирации, подредени според приоритета си. 17R трябва да се чете като “ниво на приоритет 17, с асоциативно правило от дясно на ляво”. Съответно, 7L трябва да се чете като “ниво на приоритет 7, с асоциативно правило от ляво на дясно”. Оператор с по-висок приоритет има по-високо приоритетно ниво.

Упражнение 3-8. Използвайки таблица 2.4. определетте реда на изчисление в следните смесени изрази:

(a) ! ptr == ptr->next

(b) ~ uc ^ 0377 & ui << 4

(c) ch = buf[ bp++ ] != ‘\0’

Упражнение 3-9. Трите израза по-горе се изчисляват по начин, противоречащ на намеренията на програмиста. Поставете скоби така, като считате, че програмистът би желал.

Упражнение 3-10. Защо се получава грешка от следния кодов фрагмент? Как бихте могли да я откриете?

void doSomething();

main()

{

int i = doSomething(), 0; }



3.10. Преобразуване на типове

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

Някои от преобразуванията не са безопасни; обикновено компилаторът предупреждава за тях. Преобразуването на по-широкообхватен даннов тип към по-теснообхватен е определено една небезопасна операция. Ето три примера за това:

long lval; unsigned char uc;

int (3.14159); (signed char) uc; short (lval);


Ниво
Оператор
Функция

17R
::
глобален обхват (унарна)

17R
::
класов обхват (бинарна)

16R
->,.
селектор на член

16R
[]
индекс на масив

16R
()
извикване на функция

16R
()
контруктор на тип

16R
sizeof
размер в битове

15R
++,--
увеличаване/намаляване с 1

15R
~
поразр.логическо допълване до 1

15R
!
логическо не

15R
+,-
унарни минус,плюс

15R
*,&
указаван, адрес на

15R
()
преобразуване на тип

15R
new,delete
управление на свободна памет

14L
->*,.*
селектор за член-указател

13L
*,/,%
мултипликативни оператори

12L
+,-
аритметични оператори

11L
<<,>>
побитово изместване

10L
<,<=,>,>=
операции за сравнение

9L
==,!=
равенство,неравенство

8L
&
поразредно и

7L
^
поразредно изключващо или

6L
|
поразредно или

5L
&&
логическо и

4L
||
логическо или

3L
?:
аритметичен оператор if

2R
=,*=,/=
оператори за присвояване

2R
%=,+=,-=,<<=,

2R
>>=,&=,|=,^=

1L
,
оператор запетая


Таблица 3.4. Приоритет на операциите и асоциативност

Следните два записа,

type (expr)

(type) expr

могат да бъдат наречени конвертиращи. Те представят явното изискване на програмиста за преобразуване на expr към тип type. Трите примера илюстрират потенциалната опастност от стесняването при преобразуването на типовете.

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

// 3.14159 != 3.0

3=14159 != double (int (3.141559) );

При втория случай интерпретацията на битовата схема ще бъде променена за половината от възможните стойности (128 255). Най-левият бит сега е знаков бит.

При третия случай за всяка стойност на lval, за която са необходими битове, повече от тези за short, резултатът от преобразуването е недефиниран.

Някои пребразувания са безопасни върху някои машини, но при други предизвикват стесняване. За повечето машини, например, числото int има същия размер както short или long, но не и както двете. Едно от следните преобразувания няма да бъде безопасно върху произволна машина, която не реализира int, short и long в три различни размера.

unsigned short us;

unsigned int ui;

int( us );

long( ui );

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

Неявно преобразуване на типове

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

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

void ff( int );

int val = 3.14159; // converts to int 3

ff( 3.14159 ); // converts to int 3

И в двата случая константата от тип double 3.14159 се преобразува към тип int от компилатора. Може да бъде издадено пердупреждение когато конвертирането предизвиква стесняване. При аритметичните изрази преобразуването се насочвакъм по-широкообхватни типове данни. Например,

val + 3.141559;

По-широкообхватният тип данни в този аритметичен израз е типа double. val неявно се конвертира към типа double чрез разширяване (наричано също повишаване на типа).

Нейната стойност 3 става 3.0 и се добавя към 3.141519. Резултатът от израза е 6.14159.

Забележете, че стойността на val остава 3. Операцията за преобразуване на типа се прилага върху копие на стойността на val. Променливата не се записва в процеса на преобразуване на типа.val = val + 3.14159;

В този израз има две конвертирания. Стойността на val се повишава до тип double. Резултатът 6.14159 се свива до типа int. Получената стойност се присвоява на val. val сега съдържа стойността 6.

Процедурата е съвсем същата когата изразът е записан така:val += 3.14159;

Например, стойността-резултат на следните два израза е 23, а не 20:

int i = 10;

i *= 2.3; // 23,

not 20

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

Явно преобразуване

Може да се каже, че е малко разточително да се изпълняват две операции за конвертиране в един смесен оператор i = i + 3.14159. Понеже типът на резултата е int изглежда по-разумно да се свива операнда от тип double вместо да се разширява i до тип double и след това сумата да се свива до int.

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

i = i + int(3.14159);

Сега 3.14159 се конвертира към тип int със стойност 3. Тази стойност се добавя към, и после се присвоява на i.

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

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

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

int i;

void *vp;

int *ip = &i;

double *dp;

vp = ip; // ok:

explicit castdp =vp; // error:

no standart convertion:

unsafe

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

dp = (int*)vp; // ok:

explicit cast*dp = 3=14; // trouble if dp addresses i

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

Даден е следния набор от идентификатори:

char ch;

unsigned char unChar;

short sh;

unsigned short unShort;

int intVal;

unsigned int unInt;

long longVal;

float f1;

Упражнение 3-11. Определете кои от следните присвоявания не са безопсни поради възможното стесняване:

(a) sh = intVal (e) longVal = unInt (b)intVal = longVal

(f) unInt = f1

(c) sh = unChar

(g) intVal = unShort

Упражнение 3-12. Определете типа на следните изрази:

(a) ‘a’ - 3

(b) intVal * longVal - ch

(c) f1 + longVal / sh

(d) unInt + (unsigned int) longVal

(e) ch + unChar + longVal + unInt



3.11. Оператори

Операторите са най-малките изпълними единици в една С++ програма. Те се разделят посредством точка и запетая; най-простата форма на оператор е празен или нулев оператор. Празният оператор изглежда така:

; // null statement

Този оператор е полезен в случаите, когато синтаксисът на езика изисква наличието на оператор, но не и логиката на програмата. Това понякога се случва при операторите за цикъл while и for. Например,

while ( *string++ = *inBuf++ ); // null statement

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

int val;; // additional null statement

Това е съвкупност от два оператора: декларативен оператор int val и празен оператор.

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

Съставни оператори и блокове

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

if ( account.balance - withdrawal < 0 )

{ // compound statement

issueNotice( account.number );

chargePenalty( account.number );

}

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

Съставен оператор, който съдържа един или повече декларативни оператори, се нарича блок. Блоковете се разглеждат подробно в Раздел 3.10 при обсъждането на областите на действие.

3.12. Оператори за управление

Подразбиращият се начин за изпълнение на операторите в една програма е последователен. Изпълнението на всяка С++ програма започва от първия оператор на main(). След това се изпълнява всеки следващ оператор. Когато се изпълни последния оператор изпълнението на програмата приключва.

Като изключим най-простите програми, последователното изпълнение на програмата не съответствува на проблемите, които трябва да бъдат решавани. В примерните програми, които вече разгледахме, се запознахме с условния оператор if и операторите за цикъл while и for. Следващият раздел преглежда целият набор от С++ оператори.

3.13. Операторът if

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

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

if ( expression )

statement;

Изразът expression трябва да бъде затворен в скоби. Ако той има ненулева стойност се приема, че условието има стойност истина и се изпълнява оператора statement.

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

1. Ако текущата стойност е равна на минималната стойност трябва броячът да бъде увеличен с единица.

2. Ако текущата стойност е по-малка от минималната стойност на минималната се присвоява текущата и броячът става 1. Стойностите на масива ще бъдат преглеждани като се използува оператора for. За намирането на най-малкия елемент трябва да бъде прегледан всеки елемент на масива.

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

int minVal = ? // what value

?int occurs = ? // what value ?

minVal трябва да бъде инициализирана с една начална стойност, спрямо която да бъдат сравнявани елементите на масива. Един подход при инициализирането на minVal е да се използува най-голямото възможно цяло число. Инициализирането на occurs с 0 би работило във всички случаи. Това, обаче, не е най-добрата стратегия.

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

Ако масивът е записан в низходящ ред, то последният елементе минималната стойност на масива. В този случай са необходими n присвоявания на стойност на minVal.

Масивът не е подреден и елементите са въведени в произволен ред. Може да се каже, че минималната стойност ще бъде намерена някъде в средата. При този среден случай вероятно половината от елементите ще бъдат присвоени на minVal. Във всички случаи ако minVal бъде инициализирана със стойността на първия елемент на масива със сигурност ще бъде спестено едно присвояване. Освен това ще е необходимо сравняването само на n-1 елемента. Такава ще бъде и стратегията на функцията. Ще бъдат необходими два оператора if:

if minVal > ia[ i ] ... // new minVal

if minVal == ia[ i ] ... // another occurence of//

minVal

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

if ( minVal > ia[ i ] )

minVal = ia[ i ];

occurs = 1; // not part of if statement

В противоречие с намерението на пограмиста операторът

occurs = 1;

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

if ( minVal > ia[ i ] )

{

minVal = ia[ i ];

occurs = 1;

}

Вторият оператор if изглежда така:

if ( minVal == ia[ i ] )

++occurs;

Забележете, че реда на операторите if е съществен. Във функцията ни occurs винаги ще има стойност над 1 ако поставим операторите в следния ред:

if ( minVal > ia[ i ] )

{

minVal = ia[ i ];

occurs = 1; }// potential error

//iff minVal// has just been set to ia[i]

if ( minVal == ia[ i ] )++occurs;

Изпълнението на двата оператора if за една и съща стойност е не само потенциално опасно, но и ненужно. Един и същ елемент не може да бъде едновременно по-малък от minVal и равен на него. Ако едното условие има стойност истина, то другото спокойно може да бъде игнорирано. За този вид условия операторът if предлага и else клауза.

if-else операторът има следния синтаксис:

if ( expression ) statement-1;

else statement-2;

Ако expresson има ненулева стойност, условието се приема за истинно и се изпълнява statement-1; иначе се изпълнява statement-2. Забележете, че ако statement-1 не е съставен оператор той трябва за бъде завършен с точка и запетая. Програмистът, запознат с Pascal или Ada, при които записът с точка и запетая е неправилен, често пропускат да ги напишат. Например,

if ( minVal == ia[ i ] )++occurs; // terminating ‘;’

required else if ( minVal > ia[ i ] )

{

minVal = ia[ i ];

occurs = 1;

}

В този пример операторът statement-2 е също оператор if. Ако minVal е по-малко от елемента не се прави нищо.

В следващия пример винаги се изпълнява един от трите оператора:

if ( minVal < ia[ i ] ); // null

statement else if ( minVal > ia[ i ] )

{ minVal = ia[ i ];

occurs = 1;

}else // minVal == ia[ i ]

++occurs;

Операторът if-else внася известно двусмислие в текста на програмата, което се нарича проблем на объркващото else. Този проблем се появява когато един оператор има повече if отколкото else клаузи. Въпроса, който възниква е с кое if се свързва допълнителната else клауза. Например,

if ( minVal <= ia[ i ] )

if ( minVal == ia[ i ] )

++occurs;

else

{

minVal = ia[ i ];

occurs = 1;

}

Начинът на записване на оператора показва, че програмистът вярва, че else трябва да се свърже с външната if клауза. В С++, обаче, двусмислието на записа с объркващото else се избягва като се приема, че то се свързва с последния незатворен if. В този случай операторът if-else ще бъде изпълнен както следва:

if ( minVal <= ia[ i ]

{ // effect of dangling-else resolution

if ( minVal == ia[ i ] ) ++occurs;

else

{

minVal = ia[ i ];

occurs = 1;

}}

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

if ( minVal <= ia[ i ]

{ // override the default resolution

if ( minVal == ia[ i ] ) ++occurs;}else

{

minVal = ia[ i ];

occurs = 1;

}

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

Раздел 2.8 въвежда класа IntArray. Ето реализацията на член-функцията min() на IntArray:

#include "IntArray.h"IntArray:

:

min( int &occurs ){

int minVal = ia[ 0 ];

occurs = 1;

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

{if ( minVal == ia[ i ] )++occurs;

elseif ( minVal > ia[ i ] )

{

minVal = ia[ i ];occurs = 1;}

}

return minVal;}

За да върне както минималната стойност, така и броя на появяванията й, min() използува един аргумент от тип указател към integer. Аргументът от тип указател подава стойността за запис на фактическия аргумент на функцията; аргумент, който не е от тип указател подава стойност за четене към функцията. Подаването-на-аргумент-чрез-указател позволява да му бъде присвоявана стойност от вътрешността на функцията. Ето един пимер за използуването му:

#include <stream.h>

#include "IntArray.h"

IntArray myArray;

main()

{// initialize the array in descending order

for ( int i = myArray.getSize()-2; i >= 0; --i )

myArray[ i ] = i;// insert a second copy of 0q the lowest value

myArray[ myArray=getSize()-1 ] = 0;

int number = 0;

int low = myArray.min( number );

cout << "\nlow: " << low<< "number:

number << "\n";

}

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

low:

0 number:

2

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

Упражнение 3-13. Променете декларацията на occurs от списъка на аргументите на min(), така че да не бъде от тип указател и изпълнете програмата отново.



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

Няколко вложени if-else оператори често може да бъдат синтактично правилни и въпреки това да не изразяват правилно намеренията на програмиста. Например, едно свързване на if със else клаузата, което противоречи на намеренията на програмиста, често може да мине незабелязано. Някои промени на оператори също могат да се окажат източници на грешки. С++ предлага един алтернативен метод за избор между множество от взаимноизключващи се възможности. Това е оператора switch. Например, да предположим, че ни се налага да преброим броя на появите на всяка от петте гласни в случайно избран откъс от текст. (Традиционното мнение е, че “е” е най-често срещаната гласна). Нашата програма ще има следния алгоритъм:

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

сравнява всеки символ с множеството от гласни;

ако символът е гласна добавя 1 към брояча й;

показва резултатите.

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

aCnt:

394

eCnt:

721

iCnt:

461

oCnt:

349

uCnt:

186

Програмата е реализирана като оператор swith с пет разклонения за всяка гласна. В оператора switch различните условия се отбелязват чрез етикета case. Изброяването на гласните се използува за да бъде увеличена читаемостта на програмата. Ето и самата програма:

#include <stream.h>

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, тестовият му израз се сравнява всеки от 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-етикет. Например, да припомним, че предишната ни програма не разпознаваше гласните, записани с главни букви - ето правилната реализация:

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 може внезапно да се окаже съществена.

Упражнение 3-14. Модифицирайте програмата така, че да бъдат броени и прочетените интервали.

Упражнение 3-15. Изменете програмата така, че да бъдат броени следните двубуквени последователности:

ff, f1 и fi.

Итеративни оператори

Много програмни действия изискват прилагането на определен брой оператори върху съвкупност от подобни обекти. Една банкова програма може да бъде написана за да обработва чекове. За всеки чек програмата трябва:

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

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

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

- да дебитира сметката за сумата на чека и

- да запише измененията.

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

Един цикъл изпълнява операторите от тялото си докато определено условие остава истина. Банковата програма вероятно ще си дефинира следното условие:

while there exists a check to be processed

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

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

С++ поддържа три конструкции за цикъл: операторите while, do и for. Основната разлика между тях е в метода за управление на цикъла. И за трите оператора истинно условие е всяка ненулева стойност. Нулевата стойност е еквивалентна на невярно управляващо условие.



3.15. Oператорът 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

}

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

Упражнение 3-17. Напишете функция, която връща броя на появите на даден символ в даден низ.

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



3.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 никога не се изчислява. Следват няколко примера за правилни изрази от този вид:

( ...; ...; ++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;}

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

Упражнение 3-20. Напишете функция, която търси дадена стойност в масив. Ако такава стойност бъде намерена, да връща индекса й в масива. Какво трябва да прави функцията ако стойността не е намерена?



3.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. Следващият раздел разглежда тези оператори последователно.



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



3.19. Операторът continue

Операторът continue предизвиква прекратяване на текущата итерация на най-вътрешния оператор while, do или for. В случаите, когато използуваме циклите while и do,изпълнението продължава с изчисляването на управляващия израз. Когато използуваме цикъла for изпълнението продължава с пресмятането на expression-2. За разлика от оператора break, който прекратява изпълнението на цикъла, операторът continue прекратява изпълнението само на текущата итерация. Например, следващият програмен фрагмент чете от текстов файл по една дума. Обработва се всяка дума, която започва с подчертаващо тиренце; иначе се прекратява текущата итерация:

while ( cin >> inBuf )

{if ( inBuf[0] != ‘_’ ) continue; // terminate iteration// ...

process string ...



3.20. Операторът 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

jumpIntArray myArray( sz );

procassArray( myArray );

end;

}

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

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



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





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