необходим за записване на адрес в паметта. Това означава, че
еднакъв размер. Типа, асоцииран с указателя, определя как да
битовата последователност на този адрес от паметта. Ето
от оператора ("*"). В разделения със запетаи списък на
идентификатор, който искаме да ни служи като указател. В
а не като указател.
long *lp, lp2;
В примера, който следва, fp се интерпретира като
27.
float fpf, *fp2;
За по-голяма яснота се препоръчва да се записва
char *cp;
а не
char* cp;
Много често, програмистът, желаещ да дефинира по-късно втори
стойността за запис (lvalue) на даннов обект от същия тип.
присвояване дава стойността за четене (rvalue). За да се
специален оператор. Той се нарича адресен оператор и се
записва със съмвола &. Например,
int i = 1024;
указател от същия тип. В този случай адресният оператор не е
като се използува даннов обект от тип rvalue. Следните
запис lvalue на обект от различен тип. Дефинициите на uip и
че тези стойности са коректно съпоставими. Ако те не са и
приложи. Това правило се нарича правило за преобразуване на
типовете. (вж. раздел 2.10 (стр. 80) за подробности). Ако
правило няма, операторът се отбелязва като грешен. Желателно е
изпълнение.
28.
стойността на указателя представя адрес в паметта. Всеки опит
за четене или запис на този "адрес" е опасен.
ране на указател със стойността за запис на обект от друг тип.
памет.
различен поради различния размер на int и double. Освен това,
бъде различна за различните типове.
тип. Независимо от факта, че това е потенциално опасно, то би
могло да бъде направено, но само ако е описано явно. (Раздел
2.10 (стр. 84) разглежда явното преобразуване на типовете).
даннов обект. Стойността 0, когато се използува като стойност
на указател, понякога се нарича NULL. Съществува също
присвоен адрес на обект от произволен данноов тип. (Раздел
2.10 (стр. 84) разглежда указателния тип void*).
да приложите оператора *. Например,
int i = 1024;
предизвика грешка при компилация.
int *ip = &i; // ip now points to i
трябва да приложите оператора * към указателя. Например,
различни резултати, въпреки че и двата са коректни. Първият
увеличава стойността на данновия обект, който ip адресира.
29.
int i, j, k;
добавяна или изваждана цяла стойност. Този тип обработка на
стойност. Т.е., добавянето на 2 към един указател увеличава
обекта от неговия типа. Например, като допуснем, че типът char
8 или 16 в зависимост от типа му char, int или double.
Упражнение 1-3. Дадени са следните дефиниции:
са правилни? Обяснете защо.
Упражнение 1-4. На дадена променлива се присвоява една
от следните три стойности: 0, 128 и 255. Разгледайте
дефинирания даннов тип char*. Това е така, понеже цялата
указатели. Този подраздел пояснява подробно използуването на
char*. В глава 6 ще дефинираме класовия тип String.
30.
Типът на литерална низова константа представлява
указател към първия символ на низа. Това означава, че всяка
низа. Идеята е да се завърши изпълнението на цикъла, когато
всяка литерална низова константа. За нещастие програмата,
която сме написали е неправилна. Бихте ли могли да установите
указана. Т.е.,
st++ != '\0'
проверява дали адреса, сочен от st е нулевия символ, а не
дали адресираният символ е нулевия. Условието винаги ще
се добавя единица към адреса на st.
Нашата втора версия на програмата поправя тази грешка.
Тя се изпълнява до край. За нeщастие, обаче, има грешка в
изхода й. Низът, адресиран от st не се отпечатва. Бихте ли
31.
#include
низовата литерална константа. Тя е била увеличавана до тогава,
до като е ограничена от нулевия символ. Това е символа, който
програмата насочва към стандартния изход. Необходимо е някак
да се върнем на адреса на низа. Ето едно решение на този
все още е некоректен. Той има вида:
низа. st трябва да бъде отместена с единица в повече от
дължината на низа. Правилен е следния запис:
st -= len + 1;
Когато тази програма бъде компилирана и изпълнена ще получим
програмиране, обаче, тази програма все още не е съвършена.
увеличаване на st. Повторното даване на стойност на st не се
малко по-трудна за разбиране.
неясен оператор не изглежда особено опасно. Представете си,
оператори на програмата. Добавете, че програмата може да се
състои от 10,000 реда и решаваният проблем не е тривиален.
32.
нещо, добавено върху текста на съществуващата програма.
логическа грешка в проекта й. По-добро решение е да се
коригира недостатъка на първоначалния проект. Едно възможно
инициализира със st. Например,
st, докато st остава непроменена.
други програми. Според записаното до момента, няма начин друга
гореспоменатия текст. Тази алтернатива е особено разточителна.
изчисляваща дължината на низ и поставена в отделна функция. Тя
използващи системата. Ето една примерна дефиниция на функцията
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.
Сподели с приятели: