Автор Тема: С С++ разбираема теория част 3 -Изрази и оператори  (Прочетена 12358 пъти)

zbytsam

  • Заклет Роботостроител
  • *****
  • Публикации: 256
    • Профил
    • http://genadi.masoko.net
Глава 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. Операторът 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; }

.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 :tt" << sizeof( int );
cout << "nint* :tt" << sizeof( int* );
cout << "nint& :tt" << sizeof( int& );
cout << "nint[3] :t" << sizeof( int[3] );
cout << "nn"; // 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


Операторът ("~") обръща битовете на операнда си. Всеки бит със стойност 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() != `�ґ )

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

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

while ( (ch = nextChar()) != `�ґ )

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

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

(a) ! ptr == ptr->next
(b) ~ uc ^ 0377 & ui << 4
(c) ch = buf[ bp++ ] != `�ґ

Упражнение 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 , оператор запетая


Следните два записа,
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. За намирането на най-малкия елемент
гр. София