Глава 2: Типове данни в С++
Съдържание на втора глава :

2.1. Константни стойности

2.2. Променливи
2.3. Указателни типове
2.4. Съотнасящи типове (reference types)
2.5. Константни типове
2.6. Изброими типове
2.7. Тип масив
2.8. Тип клас




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



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

Първата стъпка при усвояването на С++ - разбирането на ба-зисния език - е тема на тази и следващата глава. Тази глава обсъжда предварително дифинираните типове данни пояснява механизма за конструиране на нови типове данни, докато глава 2 разглежда предварително дефинираните операции и оператори. Текстът на програмата, която пишем, както и данните, които обработваме, са записани в паметта на компютъра като последователност от битове. Всеки бит представлява единична клетка, където могат да се съдържат стойностите 0 или 1. На физичен език тези стойности са електрически заряди, съответ-ствуващи на “off” или “on”. Обикновено част от паметта на компютъра изглежда така:

...00011011011100010110010000111011...

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

Върху последователността от битове се налага структура като се счита, че те са групирани в байтове и думи. Най - общо казано, байтът е съвкупност от 8 бита. Обикновено една дума се образува от 16 или 32 бита. Размерът на байта и думата варират между различните компютри. За тези стойности често се казва, че са машинно зависими. Фигура 1.1. показва горната последователност от битове, организирана в четири адресуеми редици от байтове.

Организацията на паметта ни позволява да се обръщаме към подходяща съвкупност от битове. По такъв начин вече е възможно да говорим за думата на адрес 1024 или за байта на адрес 1040, което ни позволява да казваме например, че байта на адрес 1032 не е равен на байта от адрес 1048.

Но все още не е възможно да се говори смислено за съдържанието на байта на адрес 1032. Защо? Защото не знаем как да интерпретираме неговата битова последователност. За да говорим за значението на байта от адрес 1032, ние трябва да знаем типа на стойността, която е представена.

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

1024 0 0 0 1 1 0 1 1

1032 0 1 1 1 0 0 0 1

1040 0 1 1 0 0 1 0 0

1048 0 0 1 1 1 0 1 1

Фиг. 1.1 Адресуема машинна памет

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

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

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

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

С++ предлага също short и long integer типове. Фактическият размер на тези типове е машинно зависим. Типовете char, short, int и long се наричат цели типове. Целите типове могат да бъдат със или без знак (signed/unsigned). Разликата се проявява в предназначението на най-левия бит на типа. Ако типът има знак,най-левият бит се интерпретира като знаков бит, а останалите битове представят стойността. Ако типът представя беззнакова стойност, всички битове определят стойността. Ако знаковият бит има съдържание 1, стойността се интерпретира като отрицателна; ако е 0, като положителна. Един 8-битов signed char може да представи стойностите от -128 до 127; а unsigned char - от 0 до 255.

Типовете float и double представят реални числа с единична и двойна точност. Обикновено типът float се представя в една дума, а double - в две. Истинският размер е машинно зависим. Изборът на типа данни се определя от размера на стойностите, които трябва да бъдат записвани. Например, ако стойностите никога не надхвърлят 255 и не са по-малки от 0, тогава типът unsigned char е подходящ. Обаче, ако се очаква стойностите да надхвърлят 255, е необходимо да се избере някой от по-големите даннови типове.

Третият тип данни, представящ реални числа long double, вероятно ще бъде добавен в близко бъдеще. Long double е предложен за включване към стандарта на езика C ANSI.

2.1. Константни стойности

Когато в дадена програма се появява стойност като 1, напри-мер, тя се приема за литерална константа: литерална, защото можем да говорим за нея само като за стойност, константа, защото стойността й не може да бъде променяна. Всеки литерал има съответен тип. 1, например е от тип int. 3.14159 е литерална константа от тип double. Считаме литералните константи за неадресуеми; въпреки, че тяхната стойност е разположена някъде в паметта, достъпът до този адрес не е съществен.

Целите литерални константи могат да бъдат написани в десетичен, осмичен или шестнадесетичен вид. ( Това не променя битовото представяне на стойността.) Стойността 20, например, може да бъде записана по един от следните три начина:

20 // десетичен

024 // осмичен

0х14 // шестнадесетичен

Водещата нула за литерална константа от цял тип указва, че константата е от осмичен тип. Представяне, използуващо 0х или 0Х в началото на константата, указва, че тя е в шестнадесетичен запис. (Приложение А обсъжда отпечатването на стойности в осмичен и шестнадесетичен запис).

Всяка цяла литерална константа може да бъде дефинирана от тип long чрез записване на L или l след стойността й. (Буквата L може да бъде главна или малка). Използуването на малка буква l не се препоръчва, понеже лесно може да бъде сбъркана с цифрата 1. По подобен начин цяла литерална константа може да бъде дефинирана като unsigned чрез добавяне на U или u след стойността й. Литерална константа от тип unsigned long може също да се дефинира. Например,

128u 1024UL 1L 8Lu

Реалните литерални константи могат да бъдат записвани чрез експонента или по обичайния начин. При първото представяне екс-понентата може да бъде записана като се използуват буквите Е или е. Реална литерална константа може да бъде дефинирана и от тип float чрез записване на F или f след стойността й. Ето няколко примера за реални литерални константи:

3.14159F 0.1f 0.0

3e1 1.0E-3 2.

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

‘a’ ‘2’ ‘,’ ‘ ‘ (blank)

Непечатуемите символи, единичните или двойните кавички, както и обърнатата наклонена черта могат да бъдат представени чрез следните escape - последователности:

newline \n

horizontal tab \t

vertical tab \v

backspace \b

carrige return \r

formfeed \f

alert (bell) \a

backslash \\

question mark \?

single quote \’

double quote \"

Може да бъде използувана и обобщена escape - последователност. Тя изглежда така:

\ооо

където ооо представлява последователност от една, две или три осмични цифри. Стойността, представена чрез осмичните цифри, представлява числената стойност на символа в символния набор на машината. Примерите, които следват, представляват литерални константи, като се използува символния набор ASCII:

\7 (bell) \14 (newLine)

\0 (null) \062 (‘2’)

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

"" (null string)

"a"

"\nCC\toptions\tfile:[cC]\n"

"a multi-line \

string literal signal its \

continuation with a backslash"

Низовият литерал е от тип масив от символи. Той се състои от низов литерал и ограничаващия символ null, добавен от компилатора. Например, докато ‘a’ представя единичния символ а, то “a” се записва като символа а, следван от символа null. Символът null се използува за отбелязване на края на низа.



2.2. Променливи

Представете си, че Ви е дадена задача да изчислите 2 на степен 10. Нашият първи опит би могъл да изглежда така:

#include <striam.h>

main()

{// a first solution

cout << 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2;

cout << "\n";

return 0;

}

Написаното работи, въпреки че ще ни се наложи да преброим два или три пъти дали сме записали константата 2 точно 10 пъти. Само тогава ще бъдем доволни. Нашата програма правилно дава отговор 1024.

Сега обаче, ни се налага да изчислим 2, повдигнато на 17 степен, а след това на 23. Неприятно е да променяме програмата си всеки път. Още по-лошо, изглежда поразително лесно да се направи грешка като се постави една двойка в повечеили по-малко. Обаче, понеже сме внимателни, ние избягваме грешките.

Накрая ни се налага да направим таблица, която да съдържа степените на двойката от 0 до 31. Ако използуваме литерални константи в директни кодови последователности ще ни бъдат нобходими 64 реда от следния вид:

cout << "2 raised to the power of X\t";

cout << 2 * ... * 2;

където Х ще се увеличава с единица за всяка кодова двойка.

В този момент, а може би и по-рано, ние осъзнаваме, че трябва да има по-добър начин. Както и наистина има. Решението изисква въвеждането на две понятия, които все още не са формално дефинирани:

1. Променливи, които позволяват да се съхраняват и възстановяват стойности.

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

#include <stream.h>

main()

{// a second more general solution

int value = 2;

int pow = 10;

cout << value<< " raised to the power of "

<< pow << ": \t";

for ( int i = 1, res = 1; i <= pow; ++i )

{ res = res * value;}

cout << res << "\n";

return 0;

}

Операторът, започващ с for, се нарича оператор за цикъл: докато i е по-малко или равно на pow, се изпълнява тялото на for, затворено във фигурни скоби. Цикълът for се нарича поточно управляващ оператор. (Програмните орератори са описани подробно в гл. 2).

value, pow, res и i са променливи, които позволяват да се съхраняват, променят и възстановяват стойности. Те са тема на следващите подглави. Първо, обаче, нека приложим друго ниво на обобщаване на програмата като отделим част от програмата, която изчислява степента на величината и да я дефинираме като отделна функция.

Всяка задача, която изисква изчисляването на някаква степен на дадена стойност, сега може просто да извика pow() с подходящо множество отаргументи. Исканата таблица от степени на двойката сега може да бъде получена по следния начин:

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

Степени на 2




0: 1
1: 2
2: 4
3: 8
4: 16

5: 32
6: 64
7: 128
8: 256
9: 512

10: 1024
11: 2048
12: 4096
13: 8192
14: 16384




Таблица 1.1 Степени на 2

Тази реализация на pow() не проверява онези особени случаи, когато имаме повдигане на отрицателна степен или стойността - резултат е много голяма.

Упражнение 1-1. Какво ще стане ако pow() бъде извикана с отрицателен втори аргумент? Как може да бъде променена pow() за да обработва това?

Упражнение 1-2. Всеки тип данни има долна и горна граница за стойностите, които може да поддържа, определени от броя на битовете, отделени за представянето му. Как това може да се отрази на pow()? Как да бъде променена функцията pow() за да обработва повиквания като pow( 2, 48 )?

Какво е променлива?

Всяка променлива се идентифицира от име, дефинирано от потребителя. Тя има и съответен тип. Например, следващият оператор дефинира променлива ch от тип char:

char ch;

char спецификатор на тип. short, int, long, float и double също представят типови спецификации. Изобщо, всяка декларация трябва да започва с типов спецификатор. Типовете на данните определят количеството памет, отделено за променливата, както и набора от операции, които могат да бъдат прилагани над този тип данни. (За нашите предположения char ще има размер в битове 8).

Както променливите, така и константите се съхраняват в паметта и са свързани с определен тип. Разликата се състои в това, че променливите са адресуеми. Т.е., има две стойности, свързани с дадена променлива:

1. Нейната стойност, съхранена на някакво място в паметта. Това поянкога се нарича нейна rvalue (произнася се “are-value”).

2. Стойността, определяща местоположението й; т.е., адреса в паметта, където е записана величината. Това понякога се нарича нейна lvalue (произнася се “ell-value”).

В израза

ch = ch - ‘0’;

променливата ch се намира както от ляво така и отдясно на оператора за присвояване. Написана от ляво, тя трябва да бъде прочетена. Стойността й се извлича от местоположението й в паметта. След това символният литерал се изважда от тази стойност. Терминът rvalue произлиза от местоположението на променливата в дясно на оператора за присвояване. Тя може да бъде четена, но не и променяна. За нея може да се мисли като за стойност за четене.

Написана от дясно, променливата ch ще бъде записвана. Резултатът от операцията изваждане се записва на мястото за стойност на ch върху предходната стойност. Терминът lvalue произлиза от разположението на променливата от лявата страна на оператора за присвояване. За нея може да се мисли като за стойност на местоположение. ch се означава като обект. Всеки обект представя някаква област от паметта. ch представя област от паметта с размер 1 байт.

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

Декларацията на променливата обявява, че променливата съществува и е дефинирана някъде. Тя се състои от името на променливата, типа й и ключовата дума extern. (За повече информация, вж. раздел 4.9 Програмен обхват). Декларацията не е дефиниция. Тя не предизвиква заделяне на място в памет. По-скоро тя едно твърдение, че дефиницията на променливата съществува някъде в текста на програмата. Една променлива може да бъде декларирана неколкократно в програмата.

В С++ всяка променлива трябва да бъде дефинирана или декларирана в програмата преди да може да бъде използувана.

Име на променлива

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

В С++ има набор от думи, предназначени за използуване от езика като ключови думи. Предварително дефинираните типови спецификатори, например, са запазени думи. Идентификаторите, които са ключови думи, не могат да бъдат използувани като програмни идентификатори. Таблица 1.2 дава списък на запазените ключови думи в С++.

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

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

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

- Идентификаторите, които се състоят от няколко думи, се записват или с разделящо подчертаващо тире или като се използуват главни букви за всяка включена дума. Например, може да се напише is_empty или isEmpty, но не isempty.

Забележете, че template е предполагаема ключова дума за възможно бъдещо разширение на С++ за поддържане на параме-тризирани типове.




asmdelete
If
register
template
auto
do

Inline
Return
try
break
double
default

Int
Short
typedef
case
else
this

Long
Signed
union
goto
catch
enum

New
Sizeof
unsigned
char
extern
protected

Operator
Static
virtual
class
float
public

Overload
Struct
void
while
const
for

Private
Switch
volatile
continue
friend




Таблица 1.2 Ключови думи в С++



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

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

double salary;

double wage;

int month;

int day;

int year;

unsigned long distance;

Когато се дефинира повече от един идентификатор за даден тип, списъкът от идентификатори, записан след спецификатора на тип, се разделя чрез запетаи. Този списък може да бъде разположен на някол-ко реда. Ограничава се от точка и запетая. Например, предходните дефиниции могат да бъдат записани по следния начин:

double salary, wage;

int month,

day, year;

unsigned long distance;

Всяка проста дефиниция определя типа и идентификатора на променливата. Тя не дава начална стойност. За променлива, която няма начална стойност, се казва че е неинициализирана. Всяка неинициализирана променлива фактически има стойност; но по-скоро може да се каже, че стойността й е недефинирана. Това е така, понеже паметта, отделена за съхраняване на променливата не е изтрита. Просто е останало това, което е било записано в паметта при предходното използуване на тази памет. Когато се чете една неинициализирана променлива случайната битова после-дователност се интерпретира като нейна стойност. Тази стойност ще се променя за различните изпълнения на програмата. Следната примерна програма илюстрира случайния характер на неини-циализираните данни.

#include <stream.h>

const iterations = 2;

void func()

{// illustrate danger of uninitialized variables

int value1, value2; // uninitialized

static int depth = 0;

if ( depth < iterations )

{

++depth;

func();

}

else depth = 0;

cout << "\nvalue1:\t" << value1;

cout << "\nvalue2:\t" << value2;

cout << "\tsum:\t" << value1 + value2;

}

main()

{

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

func();

}

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

value1: 0 value2: 74924 sum: 74924

value1: 0 value2: 68748 sum: 68748

value1: 0 value2: 68756 sum: 68756

value1: 148620 value2: 2350 sum: 150970

value1: 2147479844 value2: 671088640 sum: -1476398812

value1: 0 value2: 68756 sum: 68756



В тази програма iterations се използува като константа. Това се отбелязва с ключовата дума const. Константите се разглеждат в раздел 1.5 на тази глава. depth представлява локална статична променлива.

Значението на думата static се разяснява в раздел 3.10 при обсъждането на обхвата. func() е описана като рекурсивна функция. Раздел 4.1 разглежда рекурсивните функции.

В дефиницията на една прменлива може да й се даде първона-чална стойност. За променлива, на която е дадена първоначална стойност в декларацията се казва, че е инициализирана. Ето няколко примера за инициализиране на променливи:

#include <math.h>

double price = 109.99, discount = 0.16,

salePrice = price * discount;

int val = getValue();

unsigned absVal = abs ( val );

Предварително дефинираната функция abs(), намираща се в библиотеката math, връща абсолютната стойност на аргумента си. getValue() е функция, дефинирана от потребителя, която връща случайно цяло число. Променливите могат да бъдат инициализирани със произволни сложни изрази.



2.3. Указателни типове

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

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

Паметта, отделена за един указател, има размер, необходим за записване на адрес в паметта. Това означава, че указатели от тип int и указатели от тип double имат обикновено еднакъв размер. Типа, асоцииран с указателя, определя как да бъде интерпретирано съдържанието и каква да е дължината на битовата последователност на този адрес от паметта. Ето няколко примера на дефиниции на променливи указатели:

int *ip1, *ip2;

unsigned char *ucp;

double *dp;

Дефиницията на указател се състои от идентификатор, предхождан от оператора (“*”). В разделения със запетаи списък на дефинициите операторът * трябва да предхожда всеки идентификатор, който искаме да ни служи като указател. В следващия пример lp се интерпретира като указател към променлива от тип long, а lp2 - като даннов обект от тип long, а не като указател.

long *lp, lp2;

В примера, който следва, fp се интерпретира като даннов обект от тип float, а fp2 се интерпретира като указател към променлива от тип float:

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 за подробности). Ако правило няма, операторът се отбелязва като грешен. Желателно е това да бъде извършвано, понеже не е безопасно да се прави инициализация или присвояване без преобразуващо правило и вероятно ще бъде последвано от програмна грешка по време на изпълнение.

Би следвало да бъде очевидно защо е опасно присвояването на обект от тип rvalue на указател. По дефиниция стойността на указателя представя адрес в паметта. Всеки опит за четене или запис на този “адрес” е опасен.

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

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

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

Указател от произволен тип може да получи стойност 0, като това ще показва, че в момента указателят не адресира даннов обект. Стойността 0, когато се използува като стойност на указател, понякога се нарича NULL. Съществува също специален тип на указател, void*, с който може да бъде присвоен адрес на обект от произволен даннов тип. (Раздел 3.10 разглежда указателния тип 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 адресира.

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.

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

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

Следната програма, проектирана да изчислява дължината на st, използува адресната аритметика за преглеждането на низа. Идеята е да се завърши изпълнението на цикъла, когато бъде срещнат нулевия символ, поставян от компилатора в края на всяка литерална низова константа. За нещастие програмата, която сме написали е неправилна. Бихте ли могли да установите каква е грешката?

#include <stream.h>

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 не се отпечатва. Бихте ли могли да откриете грешката?

#include <stream.h>

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 реда и решаваният проблем не е тривиален. Част от програма, подобна на тази, често се нарича кръпка – нещо, добавено върху текста на съществуващата програма.

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

char *p = st;

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

while ( *p++ != ‘\0’ )

Нека разгледаме и едно друго подобрение на нашата програма - то позволява работата ни да бъде използувана и от други програми. Според записаното до момента, няма начин друга програма да изчисли дължината на низ, освен ако не добави гореспоменатия текст. Тази алтернатива е особено разточителна. По-добрата алтернатива е да бъде обособена частта, изчисляваща дължината на низ и поставена в отделна функция. Тя вече може да бъде на разположение на всички програмисти, използващи системата. Ето една примерна дефиниция на функцията stringLength():

#include <stream.h>

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() като използуваме новата функция:

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

1. За разлика от по-ранните версии, тази реализация на функцията stringLength() не се нуждае от достъп до st след като st е била променяна, така че промените нямат значение.

2. Всички промени, извършени над стойността на st във stringLength() изчезват когато приключи изпълнението й. За st се казва, че е изпратена по стойност към функцията stringLength(). Това означава, фактически, че това, което stringLength() обработва е само копие на st. (Раздел 4.6 разглежда подробно обръщението по стойност).

stringLength() вече може да бъде викана от всяка програма, която иска да изчисли дължина на низ. За целите на програмата ни функцията main() би могла да бъде реализирана така:

...

main()

{

int len = stringLength( st );

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

return 0;

}

stringLength() прави същото, което прави и библиотечната функция strlen(). Чрез включване на стандартния заглавен файл string.h програмистът може да използува голям брой полезни функции за обрабатка на низове, като например:

char *strcpy ( char *dst, char *scr );// копира scr в dst.

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

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

int strlen( char *st );// връща дължината на st.

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

Упражнение 1-5. Обяснете разликата между 0, ‘0’ и “0”.

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

int *ip1, ip2;

char ch, *cp;

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

(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;



2.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 адресират един и същ обект. За разлика от указателя, обаче, съотносителната променлива може да бъде инициализирана и, веднъж инициализирана, не може да стане псевдоним на друг обект.

В списъка от декларации на две или повече променливи -псевдоними е необходимо да се добавя адресен оператор пред всеки идентификатор. Например,

int i;

int &f1 = i, r2 = i; // one reference, r1; one object, r2

int r1, &r2 = i; // one object, one reference, r2

int &r1 = i, &r2 = i; // two references, r1 and r2

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

int &ir = 1024;

се преобразува така:

int T1 = 1024;

int &ir = T1;

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

unsigned int ui = 20;

int &ir = ui;

се преобразува като

int T2 = int(ui);

int &ir = T2;

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



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

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

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

Първият проблем е свързан с четимостта на текста. Какво означава да се сравни i с 512? Какво прави цикъла, т.е., какво значение има 512? (В този пример, 512 може да се нарече “вълшебно число”, чието значение не е очевидно в контекста на използуването му. В този случай може да се каже, че числото е откъснато от средата си).

Вторият проблем е свързан с поддържането. Представете си, че дадена програма се състои от 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. Всеки опит за променяне на тази стойност някъде вътре в програмата ще пречини появата на грешка по време на компилация. Именуваната константа може да се нарече променлива само за четене.

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

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. Указатели към константни обекти най-често се дефинират като формални параметри на функции. Раздел 4.6 разгрежда използуването на указатели към константи. Програмистът може също да дефинира указател-константа. Например,

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 *const 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;



2.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. Обаче се издават предупреждения, и те не трябва да бъдат игнорирани. Например,

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



2.7. Тип масив

Масивът е съвкупност от елементи от един и същ даннов тип. Самите обекти не са именувани, а по-скоро са достъпни чрез използуването на местоположението им в масива. Този метод на достъп се нарича обикновено индексен или абонатен.

Например,

int i;

декларира единичен обект от цял тип, докато

int ia[ 10 ];

декларира масив от 10 такива обекта. Всеки обект се нарича елемент на масива ia. Така

i = ia[ 7 ];

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

присвоява на елемента с индекс 7 стойността на i.

Всяка дефиниция на масив се състои от спецификатор на тип, идентификатор и размерност. Размерността, която определя броя наелементите на масива, се затваря в скоби от вида “[ ]”. Масивът мооже да имма размерност по-голяма или равна на единица. Стойността, задаваща размерността, трябва да бъде константен израз; т.е. необходимо е тази стойност да може да бъде изчислена по време на компилация. Това означава, че не може да се използува променлива за задаване на размерност на масив. Следват няколко примера за коректни дефиниции на масиви:

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 };

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

int ia[] = { 0, 1, 2 }; // an array of dimension 3

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

const SZ = 5;

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

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

char ca1[] = { ‘C’, ‘+’, ‘+’ };

char ca2[] = "C++";

ca1 има размерност 3, а ca2 има размерност 4. Следните декларации ще бъдат отбелязани като грешни:

char ch3[6] = "Pascal";// error: "Pascal" is 7 elements

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

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() се реализира така:

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

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. Намерете ги.

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

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

Дефиницията на един масив се състои от четири различни елемента: спецификатор на тип, идентификатор, индексен оператор (“[]”) и означение на размерността. Например,

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 ]; }

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

// two equivalent accessing methods

*pbuf;

buff[ 0 ];

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

*( pBuff + 1 );

buf[ 1 ];

или по-общо казано,

*( pBuff + index );

buf[ index ];

Тези два метода за адрeсиране на елементи на масив могат да бъдат използувани равностойно. Програмистите, които сега се запознават със С++ често трудно боравят с указателния синтаксис. В началото те може да искат да прилагат индексния оператор към указатели, който адресират масиви. Следващата реализация на функция, която слепва два низа, е един такъв пример.

#include <stream.h>

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;

}

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

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

char buf[] = "rampion";

main()

{

int cnt = 0;

while ( *buf )

{ ++cnt;

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

} }

Дефиницията на указател предлага само възможност да се съхранява стойност на адрес от паметта. Програмистът трябва да даде на указателя стойност, представляваща адрес на вече разположен в паметта обект или на част от такъв, преди да може да бъде използван спокойно. Това отделяне на памет често се прави и по време на изпълнение на програмата като се използува оператора new. (Вж. раздел 5.1 за повече информация).

Упражнение 1-13. Въпреки че следната програма се компилира без предупреждения или съобщения за грешки, в нея има нещо неправилно. Къде е проблема? Как ще го откриете?

char buf[] = "fiddleferns";

main()

{

char *ptr = 0;

for ( int i = 0; buf[ i ] != ‘\0’; ++i;)

ptr[ i ] = buf[ i ];

}



2.8. Тип клас

Типът клас е един дефиниран от потребителя даннов тип, който представлява съвкупност от именувани елементи от данни, може и разнотипни, както и множество от операции, проектирани да обработват тези данни. Обикновено, един клас се използува за въвеждане на нов тип данни в програмата; добре проектираният клас може да бъде използван толкова лесно, колкото и който и да е друг предварително дефиниран тип данни. Класовете се разглеждат подробно в глави от 5 до 8. Понеже те представляват фундаментална концепция за С++ в този раздел се дават обясненияза тях на основата на един обширен пример -проектирането на класа на целите масиви.

Четири от най-неприятните аспекти на предварително дефинирания тип масив са:

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то как изглежда тя:

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. Това ограничение върху достъпа е известно като скриване на информация.

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

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

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

Три от член-функциите на IntArray - за инициализация и освобождаване - използуват името на класа като свое име. Въпреки, че са дефинирани от проектанта на класа, те се извикват автоматично от компилатора. Функцията, предшествана от тилда (“~”), т.е. функцията за освобождаване, се нарича деструктор. Друтите две функции служат за инициализация; те се наричат конструктори. Конструкторът

IntArray iA2;

обработва обичайните декларации. sz представя размера на масива, желан от потребителя. Ако потребителят не зададе такъв размер, по подразбиране масивът ще съдържа ArraySize на брой елементи. Такъв е размера на iA2 в дефиницията:

IntArray iA2;

Ето една реализация на представител на IntArray(). Той се въвежда чрез оператора new. Този оператор обработва динамичното разпределение на паметта. Раздел 4.1 разглежда оператора 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.

Вторият представител на 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= ( IntArray& );

Гореспоменатият оператор е част от механизма, който позволява на класа да презарежда значението на С++ операторите, когато те се прилагат към обекти от тип клас. (Раздел 6.3) обсъжда подробно презареждането на класовите оператори). Ето и реализацията на тази функция. Забележете, че тя дава на масива-назначение размера на масива, от който се копира.

IntArray& IntArraySurprisedperator=( 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; }

Вж. Раздел 6.4 за повече информация относно оператора:

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;

За да се явява myArray[i] от лявата страна на оператор за присвояване, той трябва да има стойност за запис (lvalue). Това може да бъде направено чрез дефиниране на връщаната стойност от псевдонимен /съотнасящ/ тип ( да припомним, че този тип предлага псевдоним на дадена променлива - Раздел 4.7 разглежда връщането на стойност от тип-псевдоним). Реализацията на тази член-функция би могла да изглежда така:

int& IntArraySurprisedperator[](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 предлага минимален брой операции. Напишете списък на някои допълнителни възможности, които считате, че трябва да бъдат добавени към този клас.

Упражвение 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 явно ги е дефинирал. Това е значението на

class IntArrayRC : public IntArray

Двуеточието (“:”) дефинира IntArrayRC като произхождащ от IntArray. Такъв клас наследява (т.е. разделя) членовете на класа, от който е произлязъл. За IntArrayRC може да се мисли като за едно разширение на класа IntArray, което предлага допълнителното свойство за проверка дали индексът не надхвърля границата на масива. Исканото свойство се реализира чрез оператор по следния начин:

int& IntArrayRCSurprisedperator[]( int index )

{

rangeCheck( index );

return ia[ index ]; }

rangeCkeck() проверява всяка индексна стойност. Ако индексът е невалиден се издава съобщение за грешка, което предизвиква прекъсване на програмата. Операторът exit(), е този, който прекъсва програмата и се обръща към операционната система. Аргументите, изпратени към exit() са стойностите, които се връщат от програмата. Функцията-прототип на exit() се намира в системния заглавен файл stdlib.h. Ето реализацията на rangeCheck():

#include <stdlib.h>

#include <stream.h>

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

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() е извикана. Ето един пример:

#include <stream.h>

#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



Упражнение 1-16. Опишете други представители на класа IntArray. Какви допълнителни операции или данни могат да се добавят? Необходимо ли е да бъдат заменени някои от операциите на IntArray? Кои?



2.9. Имена на типове

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

char *winter[ 3 ];

char *spring[] = { "March", "April", "May" };

winter и spring са масиви. Всеки от тях съдържа по три елемента от тип 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-имената се използуват за подобряване на читаемостта на дефинициите на указатели към функции и указатели към член-функции на класове. (Тези типове указатели са разгледани в глави 5 и 6). Име, дефинирано чрез typedef, може също да бъде използувано за капсулиране на някои машинно зависими аспекти на програмата. За някои машини, например, типът int може да бъде достатъчно голям за да побира множество от стойности; за други - може да е необходим типа long. Тогава ще е необходимо да бъде променен само един опeратор typedef, когато дадена програма се прехвърля от една машина на друга.

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


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