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



страница5/19
Дата20.01.2017
Размер4.54 Mb.
#13105
ТипГлава
1   2   3   4   5   6   7   8   9   ...   19

Паметта, отделена за един указател, има размер,

необходим за записване на адрес в паметта. Това означава, че

указатели от тип int и указатели от тип double имат обикновено

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

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

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

няколко примера на дефиниции на променливи указатели:
int *ip1, *ip2;

unsigned char *ucp;

double *dp;
Дефиницията на указател се състои от идентификатор, предхождан

от оператора ("*"). В разделения със запетаи списък на

дефинициите операторът * трябва да предхожда всеки

идентификатор, който искаме да ни служи като указател. В

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

променлива от тип long, а lp2 - като даннов обект от тип long,

а не като указател.
long *lp, lp2;
В примера, който следва, fp се интерпретира като

даннов обект от тип float, а fp2 се интерпретира като указател

към променлива от тип float:

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


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

27.
float fpf, *fp2;
За по-голяма яснота се препоръчва да се записва
char *cp;
а не
char* cp;
Много често, програмистът, желаещ да дефинира по-късно втори

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

така:
char* cp, cp2;
Даден указател може да бъде инициализиран със

стойността за запис (lvalue) на даннов обект от същия тип.

Припомняме, че обекта, намиращ се от дясно на оператора за

присвояване дава стойността за четене (rvalue). За да се

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

специален оператор. Той се нарича адресен оператор и се

записва със съмвола &. Например,
int i = 1024;

int *ip = &i; // assign ip the addres of i


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

указател от същия тип. В този случай адресният оператор не е

необходим:
int *ip2 = ip;
Винаги се счита за грешка ако указател се инициализира

като се използува даннов обект от тип rvalue. Следните

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

компилация:


int i = 1024;

int *ip = i; //error


Грешно е също указател да се инициализира чрез стойността за

запис lvalue на обект от различен тип. Дефинициите на uip и

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

компилация:


int i = 1024, *ip = &i; // ok

unsigned int *uip = &i; // illegal

*uip2 = ip; // illegal
С++ е строго типизиран език. Всички инициализации и

присвоявания на стойности се проверяват за да сме сигурни,

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

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

приложи. Това правило се нарича правило за преобразуване на

типовете. (вж. раздел 2.10 (стр. 80) за подробности). Ако

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

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

инициализация или присвояване без преобразуващо правило и

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

изпълнение.

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


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

28.

Би следвало да бъде очевидно защо е опасно



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

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

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

По-неясно е защо съществува опастност при инициализи-

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

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

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

памет.


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

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

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

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

различен поради различния размер на int и double. Освен това,

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

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

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

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

тип. Независимо от факта, че това е потенциално опасно, то би

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

2.10 (стр. 84) разглежда явното преобразуване на типовете).

Указател от произволен тип мооже да получи стойност 0,

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

даннов обект. Стойността 0, когато се използува като стойност

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

специален тип на указател, void*, с който може да бъде

присвоен адрес на обект от произволен данноов тип. (Раздел

2.10 (стр. 84) разглежда указателния тип void*).

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

да приложите оператора *. Например,
int i = 1024;

int *ip = &i; // ip now points to i

int k = *ip; // k now contains 1024
Когато не е приложен оператора *, k ще бъде инициализирана

като адрес на i, а не чрез нейната стойност, което ще

предизвика грешка при компилация.
int *ip = &i; // ip now points to i

int k = ip; // error


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

трябва да приложите оператора * към указателя. Например,


int *ip = &i; // ip now points to i
*ip = k; // i = k;

*ip = abs( *ip ); // i = abs(i);

*ip = *ip + 1; // i = i + 1;
Следните два оператора за присвояване дават съвсем

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

оператор увеличава адреса който указателя ip съдържа; вторият

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

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

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

29.
int i, j, k;

int *ip = &i;


ip = ip + 2; // add to the address ip contains

*ip = *ip + 2; // i = i + 2;


Към адресната стойност на указателя може да бъде

добавяна или изваждана цяла стойност. Този тип обработка на

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

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

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

стойност. Т.е., добавянето на 2 към един указател увеличава

стойността на адреса, който той съдържа, с размера на два

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

заема 1 байт, int - 4 байта, а double - 8, добавянето на 2 към

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

8 или 16 в зависимост от типа му char, int или double.

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


int ival = 1024;

int *iptr;

double *dptr;
участвуващи в следните оператори за присвояване. Кои от тях

са правилни? Обяснете защо.


(a) ival = *iptr; (b) ival = iptr;

(c) *iptr = ival; (d) iptr = ival;

(e) *iptr = &ival; (f) iptr = &ival;

(g) dptr = iptr; (h) dptr = *iptr;

Упражнение 1-4. На дадена променлива се присвоява една

от следните три стойности: 0, 128 и 255. Разгледайте

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

като принадлежаща на някои от следните даннови типове:


(a) double (c) unsigned char

(b) int (d) char


Указатели към низове


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

дефинирания даннов тип char*. Това е така, понеже цялата

обработка на низове в С++ се осъществява чрез символни

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

char*. В глава 6 ще дефинираме класовия тип String.

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


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

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

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

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

като низ по следния начин:


char *st = "The expense of spirit\n";
Следната програма, проектирана да изчислява дължината

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

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

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

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

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

каква е грешката?
#include

char *st = "The expense of spirit\n";


main()

{

int len = 0;


while ( st++ != '\0' )

++len;
cout << len << ": " << st;

return 0;

}
Грешката в тази програма произтича от фаакта, че st не е

указана. Т.е.,
st++ != '\0'
проверява дали адреса, сочен от st е нулевия символ, а не

дали адресираният символ е нулевия. Условието винаги ще

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

се добавя единица към адреса на st.

Нашата втора версия на програмата поправя тази грешка.

Тя се изпълнява до край. За нeщастие, обаче, има грешка в

изхода й. Низът, адресиран от st не се отпечатва. Бихте ли

могли да откриете грешката?

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

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

31.
#include

char *st = "The expense of spirit\n";


main()

{

int len = 0;


while ( *st++ != '\0' )

++len;
cout << len << ": " << st;

return 0;

}
Грешката произтича от факта, че st вече не съдържа адреса на

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

до като е ограничена от нулевия символ. Това е символа, който

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

да се върнем на адреса на низа. Ето едно решение на този

проблем:
st -= len;

cout << len << ": " << st;


Програмата може да бъде компилирана и изпълнена. Но изходът й

все още е некоректен. Той има вида:


22: he expense of spirit
Това е свързано със самото естество на програмирането. Можете

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

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

низа. st трябва да бъде отместена с единица в повече от

дължината на низа. Правилен е следния запис:
st -= len + 1;
Когато тази програма бъде компилирана и изпълнена ще получим

следния правилен резултат:


22: The expense of spirit
Програмата вече е правилна. От гледна точка на стила на

програмиране, обаче, тази програма все още не е съвършена.

Операторът
st -= len + 1;
беше добавен с цел коригиране на грешката от директното

увеличаване на st. Повторното даване на стойност на st не се

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

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

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

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

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

оператори на програмата. Добавете, че програмата може да се

състои от 10,000 реда и решаваният проблем не е тривиален.

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

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

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

32.
нещо, добавено върху текста на съществуващата програма.

Ние закърпваме нашата програма за да коригираме

логическа грешка в проекта й. По-добро решение е да се

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

решение е да се дефинира втори символен указател и да се

инициализира със st. Например,


char *p = st;
p сега може да се използува при изчислението на дъължината на

st, докато st остава непроменена.


while ( *p++ != '\0' )
Нека разгледаме и едно друго подобрение на нашата

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

други програми. Според записаното до момента, няма начин друга

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

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

По-добрата алтернатива е да бъде обособена частта,

изчисляваща дължината на низ и поставена в отделна функция. Тя

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

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

stringLength():


#include
void stringLength( char *st )

{ // calculate length of st

int len = 0;

char *p = st;


while ( *p++ )

++len;
cout << len << ": " << st;

}
Дефиницията
char *p = st;
недостатъка на проекта на оригиналната програма. Операторът
while ( *p++ )
представя кратък запис на следното:
while ( *p++ != '\0' )
Сега можем да променим програмата main() като

използуваме новата функция:

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

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

33.
extern void stringLength ( char* );

char *st = "The expense of spirit\n";


main()

{

stringLength( st );



return 0;

}
Функцията stringLength() е записана във файла string.C.

Компилирането и изпълнението на тази програма може да бъде

направено така:


$ CC main.C string.C

$ a.out


22: The expense of spirit

$
Проектът на stringLength() e тясно свързан с

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

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

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

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

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

същ низ. Ако е така, низовете са еднакви.


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

равни. Ако не са, двата низа не са еднакви.


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

еднакви. Ако е така, низовете са еднакви. Иначе, те не са

еднакви.
stringLength(), както е проектирана, не може да бъде

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

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

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

оставено на програмата, извикваща stringLength(). Ето едно

ново решение на проблема:


int stringLength( char *st )

{ // return length of st

int len = 0;
while ( *st++ )

++len;
}


Читателят може да бъде изненадан като види, че в тази

версия на stringLength() отново st се увеличава директно.

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

поради следните две причини:

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

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

34.
1. За разлика от по-ранните версии, тази реализация на

функцията stringLength() не се нуждае от достъп до st след

като st е била променяна, така че промените нямат значение.
2. Всички промени, извършени над стойността на st във

stringLength() изчезват когато приключи изпълнението й. За st

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

stringLength(). Това означава, фактически, че това, което

stringLength() обработва е само копие на st. (Раздел 3.6 (стр.

117) разглежда подробно обръщението по стойност).


stringLength() вече може да бъде викана от всяка

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

програмата ни функцията main() би могла да бъде реализирана

така:
...

main()

{

int len = stringLength( st );



cout << len << ": " << st;

return 0;

}
stringLength() прави същото, което прави и библиотечната

функция strlen(). Чрез включване на стандартния заглавен файл

string.h програмистът може да използува голям брой полезни

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


// копира scr в dst.

char *strcpy ( char *dst, char *scr );


// сравнява два низа. връща 0 ако са равни.

int strcmp ( char *s1, char *s2 );


// връща дължината на st.

int strlen( char *st );


За повече подробности и пълен списък на библиотечните функции,

обработващи низове, направете справка в справочника на

библиотеките.
Упражнение 1-5. Обяснете разликата между 0, '0' и "0".
Упражнение 1-6. Дадено е следното множество от

дефиниции на променливи:


int *ip1, ip2;

char ch, *cp;


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

описаните типове. Обяснете защо?

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

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

35.
(a) ipl = "All happy families are alike";

(b) cp = 0; (c) cp = '\0';

(d) ip1 = 0; (e) ip1 = '\0';

(f) cp = &'a'; (g) cp = &ch;

(h) ip1 = ip2; (i) *ip1 = ip2;

1.4. Съотнасящи типове (reference types)


Този тип се дефинира като след спецификатора на тип се

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

да включва и инициализация. Например,
int val = 10;
int &refal = val; // ok

int &refVal12; // error: uninitialized


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

алтернативно име за обекта, с който е бил инициализиран.

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

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


refVal += 2;
добавя 2 към val, като тя става 12.
int ii = refVal;
присвоява на ii стойността на val, докато
int *pi = &refVal;
инициализира pi чрез адреса на val.

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

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

обекти. Например, стойността на израза


( *pi == refVal && pi == &refVal )
винаги е истина ако pi и refVal адресират един и същ обект. За

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

да бъде инициализирана и, веднъж инициализирана, не може да

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

В списъка от декларации на две или повече променливи -

псевдоними е необходимо да се добавя адресен оператор пред

всеки идентификатор. Например,

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


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

36.
int i;
// one reference, r1; one object, r2

int &f1 = i, r2 = i;


// one object, one reference, r2

int r1, &r2 = i;


// two references, r1 and r2

int &r1 = i, &r2 = i;


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

някаква стойност за четене rvalue. В този случай, се генерира

и инициализира една вътрешна временна променлива със стойност

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

променлива. Например, изразът
int &ir = 1024;
се преобразува така:
int T1 = 1024;

int &ir = T1;


Инициализация на псевдоним като се използува обект, който не

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

вътрешна променлива. Например, дефиницията
unsigned int ui = 20;

int &ir = ui;


се преобразува като
int T2 = int(ui);

int &ir = T2;


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

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

от потребителя класови типове.

1.5. Константни типове


Съществуват два проблема с оператора за цикъл for,

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


for (int i = 0; i < 512; ++i );
Първият проблем е свързан с четимостта на текста. Какво

означава да се сравни i с 512? Какво прави цикъла, т.е., какво

значение има 512? (В този пример, 512 може да се нарече

"вълшебно число", чието значение не е очевидно в контекста на

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

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

37.
използуването му. В този случай може да се каже, че числото е

откъснато от средата си).

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

си, че дадена програма се състои от 10,000 реда. Цикъл for от

подобен вид се явява в 4% от текста. Стойността 512 тряба да

бъде променена на 1024. Всичките 400 появявания на 512 трябва

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

екземпляр прекъсва програмата.

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

идентификатор, инициализиран с 512. Чрез избирането на

подходящо мнемонично име, напр. bufSize, ние можем да направим

програмата по-лесна за четене. Проверката сега е по-скоро

спрямо идентификатор, отколкото спрямо константа:
i < bufSize
Вече не е необходимо да бъдат променяни 400 - те появи на

константата за случая, когато се променя bufSize. По-скоро

може да бъде коригиран само реда, на който се инициализира

bufSize. Това не само значително намалява работата, но и

снижава вероятността за допускане на грешки. Цената на

решението е една допълнителна променлива. За стойността 512

сега може да се каже, че е локализирана.
int bufSize = 512; // input buffer size

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

// ...
Проблемът при това решение е, че bufSize е стойност за

запис - lvalue. Възможно е променливата bufSize случайно да

бъде променена в програмата. Например, ето една обща грешка,

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

език към С++.
// accidentally changes the value of bufSize

if ( bufSize = 1 )

// ...
В С++ "=" е оператор за присвояване, а "==" е оператор за

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

използуват "=" като оператор за проверка на равенство. Така

програмистът може случайно да промени стойността на bufSize на

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

грешка. (Често такава грешка е трудна за откриване, защото

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

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

Модификаторът на тип const дава едно решение на

проблема. Той преобразува променливата в константа. Например,


const int bufSize = 512; // input buffer size
дефинира bufSize като константа, инициализирана със стойност

512. Всеки опит за променяне на тази стойност някъде вътре в

програмата ще пречини появата на грешка по време на

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


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

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

само за четене.

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

след като е била дефинирана, тя трябва да бъде инициализирана.

Дефиницията на неинициализирана именувана константа ще

предизвика грешка по време на компилация. Например,


const double PI; // error: uninitialized const
Грешка от компилация се появява и когато адрес на именувана

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

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

Например,


const double minWage = 3.60;

double *p = &minWage; // error


*p += 1.40;
Програмистът, обаче, може да декларира указател, който да

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


const double *pc;
pc е указател към константен обект от тип double. pc, сам по

себе си, обаче, не е константа. Това означава следното:


1. pc може да бъде променян да адресира друга

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


2. Стойността на обекта, адресиран чрез pc, не може да

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


Например,
pc = &minWage; // ok

double d;

pc = &d; // ok

d = 3.14159; // ok

*pc = 3.14159; // error
Адресът на именувана константа също може да бъде

присвояван на указател към константа, такъв като pc. Указател

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

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


pc = &d;
Въпреки че d не е константа, програмистът е убеден, че

стойността й няма да бъде променяна чрез pc. Указатели към

константни обекти най-често се дефинират като формални

параметри на функции. Раздел 3.6 (стр. 119) разгрежда

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

Програмистът може също да дефинира указател-константа.

Например,

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


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

39.
int errNumb; // possible error status of program

int *const curErr = &errNumb; // constant pointer


curErr константен указател към обект от тип int. Програмистът

може да променя стойността на обекта на адрес curErr:


if ( *curErr )

{ errorHandler();

*curErr = 0;

}
но не може да променя адреса, който curErr съдържа:


curErr = &myErrNumb; // error
Може да бъде дефиниран и указател - константа към константен

обект:
const int pass = 1;

const int *const true = &pass;
В този случай не могат да бъдат променяни, както стойността на

адресирания чрез true обект, така и самия адрес.


Упражнение 1-7. Обяснете значението на следните пет

дефиниции. Определете кои от тях са правилни.


(a) int i; (d) int *const cpi;

(b) const int ic; (e) const int *consts cpic;

(c) const int *pic;
Упражнение 1-8. Кои от следните инициализации са

коректни? Обяснете защо.


(a) int i = 'a';

(b) const int ic = i;

(c) const int *pic = ⁣

(d) int *const cpi = ⁣

(e) const int *const cpic = ⁣
Упражнение 1-9. Като имате предвид дефинициите в

предишните упражнения, кажете кои от следните оператори за

присвояване са коректни? Обяснете защо.
(a) i = ic; (d) pic = cpic;

(b) pic = ⁣ (e) cpic = ⁣

(c) cpi = pic; (f) ic = *cpic;

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


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

40.
1.6. Изброими типове
Всеки изброим тип представя множество от именувани

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

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

се поддържа достъп по адрес. Като грешка се интерпретера всеки

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

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

използува ключовата дума enum и списък от разделени със

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

подразбиране на първия идентификатор се присвоява стойност 0.

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

по-голяма от тази на предхождащия го. Например, следните

декларации свързват FALSE с 0, а TRUE - с 1.


enum { FALSE, TRUE }; // FALSE == 0, TRUE == 1
Дадена стойност може явно да бъде присвоена на елемент

от изброим тип. Не е необходимо тази стойност да бъде

уникална. Както и преди, когато не беше дадено явно

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

по-голяма от тази на предхождащия го елемент. В следващия

пример FALSE и FALL се свързват със стойността 0, а PASS и

TRUE - с 1:
enum { FALSE, FALL = 0, PASS, TRUE = 1 };
На всеки изброим тип може да бъде дадено име. Всеки именуван

изброим тип дефинира уникален тип, който може да бъде

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

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


enum TestStatus { NOT_RUN = -1, FALL, PASS };

enum Boolean { FALSE, TRUE };


main()

{

const testSize = 100;



TestStatus testSuite [ testSize ];

Boolean found = FALSE;


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

testSuite [ i ] = NOT_RUN;

}
Именуваният изброим тип не беше дефиниран във версиите

на езика С++, преди 2.0. Следователно за осигуряване на

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

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

отбелязва като грешка в настоящата реализация на езика от

AT&T. Обаче се издават предупреждения, и те не трябва да бъдат

игнорирани. Например,

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


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

41.
main()

{

TestStatus test = NOT_RUN;



Boolean found = FALSE;
test = -1; // error: TestStatus = int

test = 10; // error: TestStatus = int

test = found; // error: TestStatus = Boolean

test = FALSE; // error: TestStatus = const Boolean

int st = test; // ok: implicit conversion

}
Като декларира променливата test от изброимия тип

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

дали на test се присвоява една от трите валидни стойности. И

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

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

1.7. Тип масив
Масивът е съвкупност от елементи от един и същ даннов

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

чрез използуването на местоположението им в масива. Този

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

Например,
int i;
декларира единичен обект от цял тип, докато
int ia[ 10 ];
декларира масив от 10 такива обекта. Всеки обект се нарича

елемент на масива ia. Така


i = ia[ 2 ];
присвоява на i стойността на елемента с индекс 7. Съответно,
ia[ 7 ] = i;
присвоява на елемента с индекс 7 стойността на i.

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

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

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

"[ ]". Масивът мооже да имма размерност по-голяма или равна на

единица. Стойността, задаваща размерността, трябва да бъде

константен израз; т.е. необходимо е тази стойност да може да

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

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

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

масиви:

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


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

42.
const bufSize = 512,

stackSize = 25,

maxFiles = 20,

staffSize = 27;


char inputBuffer [ bufSize ];

int tokenStack [ stackSize ];

char *fileTable [ maxFiles - 3 ];

double salaries [ staffSize ];


Забележете, че елементите на един масив се номерират,

като се започва от 0. За масив от 10 елемента правилните

индексни стойности са от 0 до 9, а не от 1 до 10. Това често

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

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

тях се инициализира със стойността на индекса си:


const SIZE = 10;

int ia[ SIZE ];


main ()

{

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



ia[ i ] = i;
}
Даден масив може да бъде да бъде инициализиран явно като се

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

запетаи. ( Такава инициализация е възможна само за масиви,

дефинирани вън от функции. Разлеката между дефинициите вътре и

вън от функции се обсъжда в глава 3). Например,
const SZ = 3;

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


За явно инициализирания масив не е необходимо да се задава

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

на записаните елементи:
// an array of dimension 3

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


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

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

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

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

са инициализирани явно ще получат стойност 0.
// ia ==> { 0, 1, 2, 0, 0 }

const SZ = 5;

int ia[ SZ ] = { 0, 1, 2 };
Масив от символи може да бъде инициализиран или чрез затворен

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

низова константа. Забележете обаче, че двата начина за

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


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

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

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


char ca1[] = { 'C', '+', '+' };

char ca2[] = "C++";


ca1 има размерност 3, а ca2 има размерност 4. Следните

декларации ще бъдат отбелязани като грешни:


// error: "Pascal" is 7 elements

char ch3[6] = "Pascal";


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

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


const int SZ = 3;

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


int ia2[] = ia; // error

int ia3[ SZ ];


ia3 = ia; // error
За да бъде копиран един масив в друг последователно трябва да

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

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

нека я наречем copyArray().

copyArray() изисква два масива, единият за да получи

стойностите, които ще бъдат копирани, а другият да съдържа

тези стойности. Ще ги наречем съответно назначение и източник.

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

Трябва да се отбележи, че ще са необходими различни

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

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

и ако истинският код на С++, реализиращ това е един и същ.(ў)

За целите на нашия пример ние ще дефинираме функцията

copyArraay() за цели числа. Първоначалното заглавие на нашата

функция ще има вида:
void copyArray( int torget[], int source[] );
Когато даден масив се изпраща като параметър на

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

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

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

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

аргумент, съдържащ размера на масива. copyArray() се реализира

така:

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



(ў) С въвеждането на параметризиран тип това ограничение ще

бъде отстранено. Вж. Приложение В за пояснения.

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

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

44.
void copyArray( int target[], int source[],

int targetSize, int sourceSize )


{

/* copy source to target

* set additional target elements to 0 */
ind upperBound = targetSize;
if ( targetSize > sourceSize )

upperBound = sourceSize;


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

target[ i ] = source[ i ];


while ( i < targetSize )

target[ i++ ] = 0;

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

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

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

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

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

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

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

на грешки.


Упражнение 1-10. Кои от следните дефиниции на масиви

са правилни? Обяснете защо.


int getSize();

int bufSize = 1024;


(a) int ia[ bufSize ]; (c) int ia[ 4 * 7 - 14 ];

(b) int ia[ getSize() ]; (d) int ia[ 2 * 7 - 14 ];


Упражнение 1-11. Защо следната инициализаця е грешна?
char st[ 11 ] = "fundamental";
Упражнение 1-12. В следващия кодов фрагмент има две

грешки, свързани с индексирането на масива ia. Намерете ги.

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

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

45.
main()

{

const max = 10;



int ia[ max ];

for ( int index = 1; index <= max; ++index )

ia[ index ] = index;
// ...

}

Многомерни масиви


Възможно е да бъдат дефинирани и многомерни масиви.

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

Например, изразът
int ia[ 4 ][ 3 ];
дефинира двумерен масив. Първата размерност се отнася за

редове, а втората - за колони. Т.е. ia е двумерен масив,

притежаващ четири колони с по три елемента. Двумерните масиви

обикновено се наричат матрици.

Многомерните масиви могат също да бъдат

инициализирани.


int ia[ 4 ][ 3 ] =

{

{ 0, 1, 2 },



{ 3, 4, 5 },

{ 6, 7, 8 },

{ 9, 10, 11 }

}
Вътрешните скоби, указващи разделянето по редове, не

са задължителни. Следната инициализация е еквивалентна на

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


int ia[4][3] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11 };
Чрез дефиницията, която следва, се инициализират първите

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

нула.
int ia[4][3] = { {0}, {3}, {6}, {9} };
Когато вътрешните скоби липсват резултатът ще бъде съвсем

различен. Със следната дефиниция


int ia[4][3] = { 0, 1, 2, 3 };
се инициализират първите три елемента на първия ред и първия

елемент на втория. Останалите елементи получават стойност 0.

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

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


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

46.
двойки скоби за всяка размерност. Например, следната двойка от

два вложени цикъла for инициализира двумерен масив.


main()

{

const rowSize = 4;



const colSize = 3;

int ia[ rowSize ][ colSize ];


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

for ( int j = 0; j < colSize; ++j )

ia[ i ][ j ] = i + j;

}
В програмните езици Паскал или Ада, многомерните

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

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

скоби. Въпреки че изразът
ia[ 1, 2 ]
представя една синтактично правилна конструкция както в С++,

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


- В Ада чрез този индекс се указва втория елемент на

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


- В С++ изразът указва третия ред на ia (да си

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

започва от 0). Стойността му е указател към тип int*,

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


В С++ индексния израз от примера
ia[ 1, 2 ]
се разглежда израз за последователно изпълнение, който връща

цяла стойност - в този случай:


ia[ 2 ]
Изразът за последователно изпълнение представлява серия от

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

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

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

изпълнение е 3.
7, 6+4, ia[0][0] = 0, 4-1; // comma expression

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


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

47.
Връзка между типовете масив и указател
Дефиницията на един масив се състои от четири различни

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

оператор ("[]") и означение на размерността. Например,
char buf[ 8 ];
дефинира buf като масив от 8 елемента от тип char.

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

оператор към идентификатора на масива, от вида
buf[ 0 ];
връща стойността на първия елемент, който се съдържа в buf.

Какво, обаче, се случва, когато бъде пропуснат

индексния оператор? Каква е стойността на самия идентифкатор

на масив?


buff;
Идентификаторът на масива представя адреса на първия елемент,

който се съдържа в buf. Това е еквивалентно на


&buf[ 0 ];
Да си припомним, че прилагането на адресният оператор към

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

обектът е от тип char, което означава, че buf трябва да връща

стойност от тип char*. Ако това е така, то един указател би

могъл да бъде и идентификатор на масив. Например,
char *pBuf = buff; // ok
pbuf и buf сега вече са еквивалентни. Всички изчисления се

извършват спрямо първия елемент на масива. За да бъде

адресиран следващия елемент, следователно, могат да се

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

еквивалентни:
// equivalent addressing methods

pBuff + 1;

&buf[ 1 ];
И изобщо
for ( int i = 0; i < arraySize; ++i )

{

pBuf + 1;



&buf[ i ];

}
От това следва, че формите

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

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

48.

// two equivalent accessing methods



*pbuf;

buff[ 0 ];


са еквивалентни. Всеки от тях връща стойността на нулевия

елемент на масива. За да получи достъп до следващия елемент

програмистът трябва да използува един от следните два начина:
*( pBuff + 1 );

buf[ 1 ];


или по-общо казано,
*( pBuff + index );

buf[ index ];


Тези два метода за адрисиране на елементи на масив

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

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

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

оператор към указатели, който адресират масиви. Седващата

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

пример.
#include

char *catString( char *st1, char *st2 )

{

// append st2 to st1 if two distinct strings


// if st1 does not address a string

// but st2 does, return st2

if ( st1 == 0 && st2 )

return st2;


// unless st1 and st2 address distinct

// strings, returns st1

if ( st2 == 0 || st1 == st2 )

return st1;


for ( int i = 0; st1[ i ] != '\0'; ++i )

; // stpe through to end of st1


for ( int j = 0; st2[ j ] !=; ++i, ++j )

st[ i ] = st[ j ];

st2[ i ] = '\0';

return st1;

}
Еквивалентността на указатели и масиви трябва да се

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

паметта. Методът, по който, обаче, идентификаторът на масив и

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

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

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

49.
Всяка дефиниция на масив предизвиква отделяне на блок

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

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

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

като константа. Следователно, опитът да бъде увеличен buf в

следващата програма е некоректен. Идентификаторът на масив не

е стойност за запис.
char buf[] = "rampion";

main()


{

int cnt = 0;

while ( *buf )

{ ++cnt;


++buf; // error: may not increment buff

}

}


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

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

даде на указателя стойност, представляваща адрес на вече

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

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

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

оператора new. (Вж. раздел 4.1(стр. 135)за повече информация).
Упражнение 1-13. Въпреки че следната програма се

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

има нещо неправилно. Къде е проблема? Как ще го откриете?
char buf[] = "fiddleferns";
main()

{

char *ptr = 0;



for ( int i = 0; buf[ i ] != '\0'; ++i;

ptr[ i ] = buf[ i ];


}

1.8. Тип клас


Типът клас е един дефиниран от потребителя даннов тип,

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

може и разнотипни, както и множество от операции, проектирани

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

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

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

колкото и който и да е друг придварително дефиниран тип данни.

Класовете се разглеждат подробно в глави от 5 до 8. Понеже те

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

дават обяснения за тях на основата на един обширен пример -

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

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


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

50.
Четири от най-неприятните аспекти на предварително

дефинирания тип масив са:


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

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

колко голям масив му е необходим. Един по гъвкав тип масив би

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

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

надхвърля размера му. Следващият програмен фрагмент, например,

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

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

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

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


const int SIZE = 25;

const int SZ = 10;

int ia[ SZ ];

// ...
main()

{

// not caugth during program execution



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

ia[ i ] = i;


// ...

}
3. Когато масив се изпраща като параметър на функция

е необходимо да се указва и размера му. Масивите - аргументи

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

известен.
4. Би било добре да можем да копираме един масив в

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


int ia[ SZ ];

int ia2[ SZ ] = ia; // not supported


В този раздел се дефинира клас, който поддържа тези четири

допълнителни свойства на масив. Освен това синтаксисът за

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

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

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

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

как изглежда тя:

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


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

51.
const ArraySize = 24; // default size
class IntArray

{

public;



// operations performed on arrays

IntArray( int sz = ArraySize );

IntArray( const IntArray& );

~IntArray() { delete ia; }

IntArray& operator = ( const IntArray& );

int& operator[]( int );

int getSize() { return size; )

protected:

// internal data representation

int size;

int *ia;

};
Една дефиниция на клас се състои от две части: глава

на клас, състояща се от ключовата дума class и име, и тяло,

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

Името на класа представя един нов тип данни. Той може

да служи за спецификатор на тип по същия начин, по който и

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

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


const int SZ = 10;

int mySize;

int ia[SZ]; // predefined array

IntArray myArray ( mySize ), iA( SZ );

IntArray *pA = &myArray;

InrArray iA2; // 24 elements by default


Тялото на класа съдържа няколко дефиниции. Елементите

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

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

свойствата му. В нашия случай целият масив се представя чрез

два елемента данни:
1. size, който съдържа броя на елементите на масива.
2. ia, който адресира паметта, където се разполагат

елементите.


Ключовите думи protected и public контролират достъпа

до елементите на класа. Елементите, описани в раздела public

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

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

чрез член-функциите на класа IntArray. Това ограничение върху

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

Изобщо, единствено на член-функциите на класа е

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

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

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


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

52.
1. Когато се налага промяна на представянето на

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

потребителските програми, които го използуват.
2. Когато се появи грешка при обработката на

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

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

преглежда цялата програма.


Три от член-функциите на IntArray - за инициализация и

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

Въпреки, че са дефинирани от проектанта на класа, те се

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

от тилда ("~"), т.е. функцията за освобождаване, се нарича

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

наричат конструктори. Конструкторът
IntArray iA2;
обработва обичайните декларации. sz представя размера на

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

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

елементи. Такъв е размера на iA2 в дефиницията:


IntArray iA2;
Ето една реализация на представител на IntArray(). Той се

въвежда чрез оператора new. Този оператор обработва

динамичното разпределение на паметта. Раздел 4.1 (стр. 135)

разглежда оператора new.


IntArray::IntArray( int sz )

{

size = sz;


// allocate an integer array of size

// and set ia to point to it

ia = new int[ size ];
// initialize array

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

ia[ i ] = 0;

}
Операторът две двуеточия :: се нарича scope оператор (за

обхват). Той указва на компилатора, че функцията IntArray е

дефинирана като член на класа IntArray. Функциите, които са

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

елементи директно. Котато напишем


size = sz;
size се отнася до данни измежду класовите член-променливи, за

които е била извикана член-функцията. В нашия пример, size е

член-данни на iA2.

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


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

53.
Вторият представител на IntArray() обработва

инициализацията на един обект от тип IntArray чрез друг. Тя се

извиква автоматично, когато се срещне дефиниция от вида:
IntArray iA3 = myArray;
Тази реализация изглежда като другата, с изключение на това,

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


IntArray::IntArray( const IntArray &iA )

{

size = iA.size;


ia = new int[ size ];
for ( int i = 0; i < size; ++i )

ia[ i ] = iA.ia[ i ];

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

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


1. Операторът точка (".") се използува, когато

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

подходящия клас на обекта.
2. Операторът стрелка ("->") се използува, котато

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

използува указател към клас.
Например, значението на израза
iA.size;
може да бъде формулирано така: Избери член-данните size от

класа на обекта iA.

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

чрез друг се обработва така:


IntArray& operator= ( IntArraay& );
Гореспоменатият оператор е част от механизма, който позволява

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

те се прилагат към обекти от тип клас. (Раздел 6.3 (стр. 254)

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

реализацията на тази функция. Забележете, че тя дава на

масива-назначение размера на масива, от който се копира.

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

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

54.
IntArray& IntArray::operator=( const IntArray &iA )

{

delete ia; // free up existing memory



size = iA.size; // resize target

ia = new int[ size ]; // get new memory

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

ia[ i ] = iA.ia[ i ] ; // copy

return *this;

}
Вж. Раздел 5.4 (стр. 196) за повече информация относно

оператора:
return *this;
Презареденият оператор за присвояване ще бъде извикван

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

IntArray на друг. Например,
ia2 = myArray;
Класът масив не би предизвиквал такъв практически интерес ако

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

Необходимо е да се поддържа следната конструкция за цикъл:
for ( int i = 0; i < upperBound; ++i )

myArray[ i ] = myArray[ i ] + 1;


където на upperBound е дадена стойността на size от обекта

myArray на класа IntArray. Всичко това може да бъде

реализирано чрез член-функциите getSize() и operator[].

getSize(), наречена функция за достъп, предлага достъп за

четене на една друга собствена стойност. Понеже тя е много

малка, дефиницията й е включена в дефиницията на класа.

UpperBound може да получи стойност като използува getSize():
upperBound = myArray.getSize();
или самата getSize() може да заамести upperBound в цикъла for:
for ( int i = 0; i < myArray.getSize(); ++i )
Функцията, реализираща индексния оператор, не е много

по-голяма от getSize(), но трябва да е така реализирана, че да

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

осигуряваща четенето се реализира просто: взема се стйността

на индекса и се връща съоответния елемент. Това е необходимо

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


int i = myArray[ someValue ];
както и от вида:
myArray[ i ] = someValue;

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


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

55.
За да се явява myArray[i] от лявата страна на оператор

за присвояване, той трябва да има стойност за запис (lvalue).

Това може да бъде направено чрез дефиниране на връщаната

стойност от псевдонимен/съотнасящ/ тип ( да припомним, че този

тип предлага псевдоним на дадена променлива - Раздел 3.7 (стр.

121) разглежда връщането на стойност от тип-псевдоним).

Реализацията на тази член-функция би могла да изглежда така:
int& IntArray::operator[](int index)

{

return ia[index];



}
Обикновено, дефиницията на класа, както и на всички

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

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

гореописания случай заглавният файл се нарича IntArray.h.

Всички програми, които класът IntArray() използува, трябва да

са включени в този заглавен файл. Съответно, член-функциите на

класа обикновено се записват в текстов файл, именуван също

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

записани във файла IntArray.C. За да използува тези функци

програмистът трябва да ги добави към своя изпълним файл.

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

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

в библиотека. Това се прави така:
$ CC -c IntArray.C

$ ar cr IntArray.a IntArray.o


ar е команда за създаване на архивна библиотека, поддържана

от системата UNIX. Символите cr, които следват, представляват

опции за командния ред. IntArray.o е един обектен файл,

съдържащ машинните инструкции, съответствуващи на С++

програмата. Той се генерира, когато на компилатора се зададе

опцията -с. IntArray.a е името, което ще бъде дадено на

библиотеката, съдържаща класа IntArray. За да използува тази

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

командния ред когато компилира програма:
$ CC main.c IntArray.a
Това ще предизвика включването на член-функциите на класа

IntArray в изпълнимия код на програмата.


Упражнение 1-14. Илюстрираният тук клас IntArray

предлага минимален брой операции. Напишете списък на някои

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

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

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

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

56.
Упражвение 1-15. Полезно би било да можем да

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

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

конструктор:


IntArray::IntArray( int *ia, int size );
Трябва да могат да бъдат поддържани следните дефиниции:
int ia[ 4 ] = { 0, 1, 2, 3 };

IntArray myIA( ia, 4 );


IntArray ни представя един важен аспект от

използуването на класовия механизъм в С++. IntArray може да

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

използуват класа IntArray по съвсем същия начин, по който и

всички останали предварително дефинирани типове данни в С++.

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


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

възможнастта за дефиниране на подтипови връзки. Например,

IntArrayRC е един тип на класа цели масиви, който също

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

на дефинирания му обхват. Той е реализиран на основата на т.н.

наследствен механизъм. Ето дефиницията на IntArrayRC:


#include "IntArray.h"
class IntArrayRC : public IntArray

{ public:

// constructors are not inherited

IntArrayRC( int = ArraySize );

int& operator[]( int );

protected:

void rangeCheck( int );

}
IntArrayRC трябва да дефинира само тези аспекти на

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

реализацията на IntArray. Той трябва да предлага:


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

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

масива.
2. функция, която извършва тази проверка.
3. собствен набор от функции за автоматична

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


Всички данни и функции на IntArray са достъпни за

IntArrayRC така, както и ако IntArrayRC явно ги е дефинирал.

Това е значението на

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


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

57.
class IntArrayRC : public IntArray
Двуеточието (":") дефинира IntArrayRC като произхождащ

от IntArray. Такъв клас наследява (т.е. разделя) членовете на

класа, от който е произлязъл. За IntArrayRC може да се мисли

като за едно разширение на класа IntArray, което предлага

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

границата на масива. Исканото свойство се реализира чрез

оператор по следния начин:
int& IntArrayRC::operator[]( int index )

{

rangeCheck( index );



return ia[ index ];

{
rangeCkeck() проверява всяка индексна стойност. Ако

индексът е невалиден се издава съобщение за грешка, което

предизвиква прекъсване на програмата. Операторът exit(), е

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

система. Аргументите, изпратени към exit() са стойностите,

които се връщат от програмата. Функцията-прототип на exit() се

намира в системния заглавен файл stdlib.h. Ето реализацията на

rangeCheck():
#include

#include


enum { ERR_RANGE = 17 };
void IntArrayRC::rangeCheck( int index )

{

if ( index < 0 || index >= size )



{

cerr << "Index out of bounds for IntArrayRC: "



<< "\n\tsize: " << size

<< "\n\tindex: " index << "\n";

exit ( ERR_RANGE );

}

}
Понеже конструкторите не са наследени от производния



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

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

неговата реализация:
// IntArrayRC need only pass its argument

// to its base class IntArray constructor

IntArrayRC::IntArrayRC( int sz )

: IntArray( sz )

() // null body
Тази част от конструктора, която започва с двуеточие,

се нарича инициализационен списък за член. Този списък

предлага механизъм, чрез който на конструктора на IntArray се

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


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

58.
изпраща неговия аргумент. Тялото на конструктора на IntArrayRC

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

задаващ размерността, на конструктора на IntArray. Ето един

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

тип IntArrayRC:
#include "IntArrayRC.h"
const size = 12;

main()


{

IntArrayRC ia( size );


// subscript error: 1..size

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

ia[ i ] = i;

}
Тази програма неправилно индексира ia от 1 до size

вместо от 0 до size-1. Когато се компилира и изпълни, тази

прорама ще даде следния изход:


Index out of bounds for IntArrayRC:

size: 12 index: 12


Както беше показано от примера, проверката за

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

добра защита при използуването на типа масив. Може да се каже,

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

извършва само по време на изпълнение на програмата. Бихме

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

IntArratRC в различни части на програмата си. Класовата

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


1. Към класа, който има наследници, наричан базов клас, могат

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

Например:
#include "IntArray.h"
void swap elements &ia, int i, int j )

{

// swap elements i and j within ia



int tmp = ia[[ i ];

ia[ i ] = ia[ j ];

ia[ j ] = tmp;

}
Към swap() могат да бъдат изпращани аргументи от клас

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

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

класови обекта:

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


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

59.
IntArray ia1;

IntArrayRC ia2;


за които са валидни следните две извиквания на swap():
swap( ia1, 4, 7 );

swap( ia2, 4, 7 );


Способността за свързване на произхождащи класови обекти с

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

на наследствените класове.

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

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

Когато извикваме


swap( ia1, 4, 7 );
трябва да бъде използуван индексният оператор на IntArray.

Когато, обаче, извикваме


swap( ia2, 4, 7 );
трябва да бъде използуван индексният оператор на IntArrayRC.

За да бъде използуван този оператор през swap() би следвало да

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

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

езиковият компилатор на С++, като се използуват механизми,

свързани с виртуални класови функции.


2. Виртуалните класови членски функции са наследени членове,

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

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

променим декларацията му в тялото на класа IntArray:


class IntArray

{

public:



virtual int& operator[]( int );

...


}
Сега при всяко обръщение към swap() ще бъде извикван

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

класа, за който swap() е извикана. Ето един пример:

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


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

60.

#include



#include "IntArray.h:

#include "IntArrayRC.h"


void swap( IntArray&, int, int);
main()

{

const size = 10;



IntArray ia1( size );

IntArrayRC ia2( size );


// error: shoud be size-1

cout << "swap() with IntArray ia1\n";

swap( ia1, 1, size );

cout << "swap() with IntArrayRC ia2\n";

swap( ia2, 1, size );

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

следния резултат:
swap() with IntArray ia1

swap() with IntArrayRC ia2

Index out of bounds for IntArrayRC:

size: 10 Index: 10


Наследствеността и виртуалните функции са двата

първични компонента на обектно - ориентираното програмиране.

Те се разглеждат в глави 7 и 8.
Упражнение 1-16. Опишете други представители на класа

IntArray. Какви допълнителни операции или данни могат да се

добавят? Необходимо ли е да бъдат заменени някои от операциите

на IntArray? Кои?

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

производни типове. Те са конструирани на основата на други

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

дефиниране на специални променливи; за тези оператори може да

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

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

така както е при дефинирането на масив от указатели:
char *winter[ 3 ];

char *spring[] = { "March", "April", "May" };


winter и springn са масиви. Всеки от тях съдържа по три

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


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

61.
елемента от тип char*. spring е инициализиран. Операторът
char *cruellestMonth = spring[ 1 ];
инициализира cruellestMonth чрез "April", символният низ,

адресиран от втория елемент на spring.

Следните два оператора за изход са еквивалентни:
main()

{

cout << "Lilacs breed in " << spring[ 1 ];



cout << "Lilacs breed in "

<< cruellestMonth;

}
Механизмът typedef предлага едно най-общо улеснение за

въвеждане на мнемонични синоними за съществуващи

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

дефинирани даннови типове. Например,
class IntArray;
typedef double wages;

typedef IntArray testScores;

typedef unsigned int bitVector;

typedef char *string;

typedef string monthTaable[3];
Тези имена на типове могат да служат като типови спецификатори

вътре в програмата:


const classSize = 93;
string myName = "stan";

wages hourly, weekly;

testScores finalExam( classSize );

monthTable summer, fall = {

"September", "October",

"November" };


Дефиницията typedef започва с ключовата дума typedef, следвана

от даннов тип и идентификатор. Идентификаторът, или името на

типа, не въвежда нов тип, а по-скоро задава синоним на

съществуващ даннов тип. Името на тип, дефинирано чрез typedef

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

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

Името на типа, зададено чрез typedef, може да служи за

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

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

Обикновено typedef-имената се използуват за подобряване на

читаемостта на дефинициите на указатели към функции и

указатели към член-функции на класове. ( Тези типове указатели

са разгледани в глави 4 и 5).

Име, дефинирано чрез typedef, може също да бъде

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

на програмата. За някои машини, например, типът int може да

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

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

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

други - може да е необходим типа long. Тогава ще е необходимо

да бъде променен само един опeратор typedef, когато дадена

програма се прехвърля от една машина на друга.

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

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

63.


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


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




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

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