Глава 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
Адресируема машинна памет, типове данни.
Други типове са адресите в паметта и машинните инструкции, които управляват работата на компютъра.
С++ предлага един предварително дефиниран набор от типове на данни, който позволява представянето на цели и реални числа и на самостоятелни символи.
Типът 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)
� (null) �62 (`2ґ)
Всяка низова литерална константа представлява съвкупност от нула или повече символа, обградени с двойни кавички. Непечатуемите символи могат да бъдат представяни чрез техните escape - последователности. Низов литерал може да заеме няколко реда от текста на програмата. Обратната наклонена черта като последен символ на реда указва, че низовият литерал продължава на следващия ред. Следва пример за низови литерални константи:
"" (null string)
"a"
"nCCtoptionstfile:[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 Xt";
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 се нарича поточно управляващ оператор.
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
Степени на 2
Тази реализация на pow() не проверява онези особени случаи, когато имаме повдигане на отрицателна степен или стойността - резултат е много голяма.
Упражнение 1-1. Какво ще стане ако pow() бъде извикана с отрицателен втори аргумент? Как може да бъде променена pow() за да обработва това?
Упражнение 1-2. Всеки тип данни има долна и горна граница за стойностите, които може да поддържа, определени от броя на битовете, отделени за представянето му. Как това може да се отрази на pow()? Как да бъде променена функцията pow() за да обработва повиквания като pow( 2, 48 )?
Какво е променлива?
Всяка променлива се идентифицира от име, дефинирано от потребителя. Тя има и съответен тип. Например, следващият оператор дефинира променлива ch от тип char:
char ch;
char спецификатор на тип. short, int, long, float и double също представят типови спецификации. Изобщо, всяка декларация трябва да започва с типов спецификатор. Типовете на данните определят количеството памет, отделено за променливата, както и набора от операции, които могат да бъдат прилагани над този тип данни. (За нашите предположения char ще има размер в битове
.
Както променливите, така и константите се съхраняват в паметта и са свързани с определен тип. Разликата се състои в това, че променливите са адресируеми. Т.е., има две стойности, свързани с дадена променлива:
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 е предполагаема ключова дума за възможно бъдещо разширение на С++ за поддържане на параме-тризирани типове.
Таблица 1.2 Ключови думи в С++
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
Дефиниции на променливи
Една проста дефиниция се състои от спецификатор на тип следван от име. Дефиницията се ограничава от точка и запетая. Ето някои примери на прости дефиниции:
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 spiritn";
Следната програма, проектирана да изчислява дължината на st, използува адресната аритметика за преглеждането на низа. Идеята е да се завърши изпълнението на цикъла, когато бъде срещнат нулевия символ, поставян от компилатора в края на всяка литерална низова константа. За нещастие програмата, която сме написали е неправилна. Бихте ли могли да установите каква е грешката?
#include <stream.h>
char *st = "The expense of spiritn";
main() {
int len = 0;
while ( st++ != `�ґ ) ++len;
cout << len << ": " << st;
return 0;
}
Грешката в тази програма произтича от факта, че st не е указана. Т.е.,
st++ != `�ґ
проверява дали адреса, сочен от st е нулевия символ, а не дали адресираният символ е нулевия. Условието винаги ще получава стойност истина, защото при всяка итерация на цикъла се добавя единица към адреса на st.
Нашата втора версия на програмата поправя тази грешка. Тя се изпълнява до край. За нeщастие, обаче, има грешка в изхода й. Низът, адресиран от st не се отпечатва. Бихте ли могли да откриете грешката?
#include <stream.h>
char *st = "The expense of spiritn";
main() {
int len = 0;
while ( *st++ != `�ґ ) ++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;
А сега може да се използува при изчислението на дъължината на st, докато st остава непроменена.
while ( *p++ != `�ґ )
Нека разгледаме и едно друго подобрение на нашата програма - то позволява работата ни да бъде използувана и от други програми. Според записаното до момента, няма начин друга програма да изчисли дължината на низ, освен ако не добави гореспоменатия текст. Тази алтернатива е особено разточителна. По-добрата алтернатива е да бъде обособена частта, изчисляваща дължината на низ и поставена в отделна функция. Тя вече може да бъде на разположение на всички програмисти, използващи системата. Ето една примерна дефиниция на функцията 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++ != `�ґ )
Сега можем да променим програмата main() като използуваме новата функция:
extern void stringLength ( char* );
char *st = "The expense of spiritn";
main() {
stringLength( st );
return 0;
}
Функцията stringLength() е записана във файла string.C. Компилирането и изпълнението на тази програма може да бъде направено така:
$ CC main.C string.C
$ a.out
Проектът на 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 = `�ґ;
(d) ip1 = 0; (e) ip1 = `�ґ;
(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. Константни типове
Съществуват два проблема с оператора за цикъ