Покажи участието

От тук може да видиш всички публикации на този потребител.


Теми - zbytsam

Страници: 1 2 [3] 4
31
Литература / Закон на Кулон
« -: Февруари 13, 2009, 11:14:12 pm »
Явлението електростатично привличане е било известно на древногръцките учени още преди новата ера. Те са знаели например, че ако разтъркаш кехлибар в котешка кожа, а стъкло в коприна, то помежду им възникват сили на привличане. Освен това на тях също им било известно, че с помощта на такива предмети може да се причини въздействие и на други предмети: например, ако докоснем кехлибара до трошичка от корк, то тя ще се отблъсква от другите коркови частици, към които се е докосвал кехлибара и привлича от тези, до които се е докосвало стъклото. Днес ние знаем, че подобно привличане и отблъскване е проява на статическото електричество. Ние наблюдаваме електростатически явления в ежедневието, когато например ни се налага буквално да разлепваме една от друга току-що изпраните и извадени от сушилнята дрехи, когато по никакъв начин не можем да приведем във форма наелектризираните и буквално настръхнали коси.

Електростатиката в съвременните разбирания започва с осъзнаването на факта, че подобно поведение (привличане и отблъскване), наблюдавано още от древните гърци, се явява следствие от съществуването в природата на два вида електрически заряди – положителни и отрицателни. В атома те са разделени. Положителните заряди са съсредоточени в атомното ядро – техни носители се явяват протоните, а електроните, явяващи се носители на отрицателните заряди, са разположени около ядрото(виж. Атома на Бор). Първият предложил идеята за това, че в природата съществуват само два типа електрически заряди, и само те са отговорни за наблюдаваните от нас електростатически явления е американският държавен деец и учен Бенджамин Франклин (Benjamin Franklin, 1706–1790). Казано на съвременен език, неговите разсъждения се свеждали до това, че ако отстраним част от отрицателно заредените електрони от веществото, то ще стане положително заредено, доколкото в нормално състояние именно отрицателният заряд на електроните компенсира положителният заряд на ядрата. Ако пък към веществото в нормално състояние се добавят допълнителни електрони, то ще придобие отрицателен заряд.

Знаейки за съществуването на електричеството в продължение на хиляди години, човек е пристъпил към научното му изучаване едва в XVIII век. (Любопитното е, че самите учени през тази епоха, заемайки се с този проблем, са отделяли електричеството в отделна от физиката наука, а себе си са наричали “електричари”.) Един от водещите първоизследователи на електричеството се явява Шарл Огюстен дьо Кулон. Грижливо изследвайки силите на взаимодействие между телата, носещи в себе си различни електростатически заряди, той формулирал закон, носещ в момента неговото име. Основно той провеждал опитите си по следният начин: различните електростатически заряди се предавали на две мънички топчета, провесени на изключително тънки нишки, след което те били доближавани. При достатъчно приближаване топчетата започвали да се привличат едно друго (при противоположна поларност на електрическите заряди) или да се отблъскват (в случаите на едноименни заряди). В резултат нишките се отклонявали от вертикалата на достатъчно голям ъгъл, при който силите на електростатичното привличане или отблъскване се уравновесявали със силите на земното претегляне. Измервайки ъгълът на отклонението и знаейки масата на топчетата и дължините на нишките, Кулон е изчислил силите на електростатично взаимодействие при различно раздалечаване на топчитата едно от друго и на базата на тези данни е извел емпирическата формула:

F  = kQq/D 2

Където Q и q са величините на електростатичните заряди, D - разстоянието между тях, а к – определена по експериментален път константа на Кулон.

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

Втората важна забележда се отнася до константата на Кулон. Когато шотландският физик - теоретик Джеймс Кларк Максуел е извел системата уравнения на Максуел за общото описание на електромагнитните полета, се изаснява, че константата на Кулон е пряко свързана със скоростта на светлината с. И накрая Алберт Айнщайн доказва, че с играе ролята на фундаментална световна константа в рамките на теорията на относителността. По такъв начин може да се проследи как най-абстрактните и универсални теории на съвременната наука поетапно са се развивали, попивайки в себе си по-рано получените резултати, започвайки с прости изводи, базирайки се на основата на елементарни физически опити

Опит на Кулон

През XVIII в. френският физик Шарл Кулон извършва първите точни количествени измервания на електричните сили. Опитна постановка, наречена Кулонова везна е показана по-долу. В стъклен съд на тънка нишка е окачена хоризонтална стъклена пръчка. На единия край на пръчката е поставено позлатено топче А, а на другия край – балансираща теглилка Т. На известно разстояние от топчето А е закрепено неподвижно такова топче В. Двете топчета се зареждат с едноименни електрични заряди. Неподвижното топче В отблъсква топчето А с електрична сила F и стъклента пръчка се завърта. За да се възстанови първоначалното положение на пръчката, трябва да се усуче нишката в противоположна посока на определен ъгъл . Тогава еластичната сила, с която усуканата нишка действа на пръчката, уравновесява електричната сила F. Като се измери ъгълът на усукване на нишката, може да се определи силата F.



Кулон прави опит при постоянен заряд на двете топчета, като променя разстоянието между тях. Той установява, че когато разстоянието нарасне 2 пъти, силата намялава 22=4 пъти. При 3 пъти по-голямо разстояние силата е 32=9 пъти по-малка. Оттук следва изводът, че електричната сила F е обратнопропорционална на квадрата на разстоянито r между зарядите



След това Кулон поддържа постоянно разстоянието между топчетата, а променя заряда на топчето В. Това става с помоща на трето топче С, еднакво с първите две. Когато незареденото топче С се допре до В, зарядите се разпределят по равно между двете топчета, тъй като те са еднакви. Така след отстраняване на С зарядът на В намалява наполовина. Измерванията показват, че електричната сила също намаля 2 пъти. Чрез подобни опити Кулон установява, че електричната сила е правопропорционална на произведението от големините на q1 и  q2

32
Литература / Електрическо поле
« -: Февруари 13, 2009, 11:09:13 pm »
Английските физици Майкъл Фарадей и Дж. Маскуел първи доказват, че наелктризираните тела взаимодействат посредством електрични полета. Всеки заряд създава електрично поле, което изпълва пространството около него и му предава нови свойства. Тези свойства се проявяват в това, че на всеки друг заряд, поставен в полето, действа сила. Електричното поле на неподвижни заряди се нарича електростатично поле.

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



Интензитет на електрическо поле

Наличието на електрично поле може да се установи с помощта на положителен заряд q0, наречен пробен заряд. Например на снимката по-долу пробния заряд q0 е поставен в точка А на електричното поле, създадено от наелктризирана с положителен заряд стъклена пръчка. Полето действа на заряда q0 с електрична сила F. Отношението на големината на силата F към големината на заряда q0 се нарича интензитет на електричното поле в точка А:





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

Посока на интензитета Е на електричното поле в дадена точка от пространството съвпада с посоката на F, с която действа на положителн заряд q0 , поставен в тази точка. Големината на интензитета се определя по формулата за интензитет на електрично поле.

Единицата за интензитет се получава от формулата. В SI силата се измерва в нютони (N), а зарядът - в кулони (C). Затова единицата за интензитет е нютон на кулон (N/C). Интензитетът Е се представвя с насочена отсечка (както е отдолу): дължината на отсечката е пропорционална на големината на интензитета

33
Литература / Наелектризиране на телата
« -: Февруари 13, 2009, 11:01:21 pm »
Да вземем пластмасова пръчка и да я натрием няколко пъти по сухи косми или вълнен парцал. Удивително, но след това пръчката придобива ново свойство: започва да привлича малки листчета хартия, други леки предмети и дори тънки струйки вода.



В миналото не е имало пластмасови пръчки. Въпреки това подобни явления били добре известни. За опити по наелектризиране предците ни използвали дървени пръчки покрити със смола, които триели във вълна. След това тези пръчки започвали да привличат сухи треви. Опитите показват, че две тела наелектризирани и ненаелектризирани винаги се привличат. Примерно пластмасова пръчка и тънка струя вода. Опитите показват също така, че две тела наелектризирани докато се трият едно в друго също се привличат. Например наелектризирани дрехи, които са се трили по тялото ни лепнат по него.

За изследване на наелектризирани тела служат специални уреди - електроскопи. Външния вид на уреда виждате на снимката отдолу. Цилиндричен корпус (1) закрит със стъкло (2). Вътре в прибора е поставен метален прът (3) с лесноподвижни пластинки. Пластмасова втулка (5) отделя металния прът от корпуса на уреда.


34
Не че форума е лош , не че имам оплаквания , но след като вече доста се познаваме , дайте да го направим по - уютен и по-лесен за сърфиране.
1. Ако искате да направим предложението за име и данни в подписа задължително. Не виждам кой има от кой да се крие - не сме форум на килърите все пак.  

2. Темми от рода " искам да направя Т1000 , научете ме" просто да се трият - нямма смисъл да се заключват след като в крайна сметка не изразяват нищо. Така подбуждат баш лапетата да пускат следваща такава.

3, За схемите и нещата , които сме установили че са грешни - да ги коригираме.

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

35
Робо-сумо / Желаещи да тестваме сумотата???
« -: Февруари 12, 2009, 03:11:55 pm »
Много съм навит да ги "сбиеме". Има ли други желаещи?

36
Искам да си набия един код , пък нямам

37
Атлетик-роботи / Най-сетне в къщата ми!!!
« -: Февруари 04, 2009, 06:55:05 pm »
След дълги дни и нощи чакане най-сетне имам 60 сантиметров хуманоид Робосапиен версия 2.  :clap:  Тепърва тунинга предстои , но за това по-нататък. Ето го:



На снимката не личи но е като малко дете на ръст :D

Ето снимка и спрямо предшественика му 30 сантиметровия робосапиен версия 1



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




За сега ще го тествам тези дни, ще пробвам управлението у с джойстик и след това мисля да се захвана с присаждането на Ардуино.

38
:-D



Този в благодарение на едни мои френдове-няма да казвам кои са :clap:  :clap:  :clap:  :clap:



Замазан изглед отпред.




А сега втория , който за сега е само осноова колела и сервота, но мисля да надмине пътрия. Изказвам големи благодарности на Слави (СПП) , за това че ми помогна толкова много,макар и лишен от всякакво време :clap:



Изглед отпред.






Изглед отзад



Двата заедно

[/img]http://media.snimka.bg/2057/010368644-big.jpg[/list]

39
Атлетик-роботи / Роботите WOWWEE
« -: Януари 31, 2009, 11:17:03 pm »
Тук ще напиша за "Уолитата" - мойте любими роботи.По-специялно ще се спра на двата хуманоида Робосапиен и Робосапиен В2.

Робосапиен



Моделът робосапиен е любимец на много хора.Създава го Марк Тилдън, бивш служител на НАСА.Висок е около 44 сантиметра.Роботът работи в три мануал режима ,има 67 програмируеми функции,Може да вдига и подхвърля леки предмети,да танцува,да се навежда и да си върти главата. Тилдън е оставил доста място за тунинг по него и Робосапиен може да стане наистина сериозна машинка. Аз все още не смея да пипна моя, но и това ще стане.Роботът ,макар и трудно откриваем,се предлага в БГ -на цена около 160 лв. и цена около 90 лв, за Китайска реплика(там определено ще се проклинате , че сте спестили 60 лева)

Малко клипчета на Робосапиен:

http://www.youtube.com/watch?v=EvFY38HYbyY

Следващата ми тема за Версия 2 на Робосапиен ,ще пусна когато получа един от тях, което се надявам да стане след броени дни.

40
Механика / Лутайки се на тема механика.
« -: Януари 30, 2009, 01:00:34 am »
Тъкмо мислех че програмирането е наи-отвратителното нещо и открих , че може да е още по-гадно.  :-D  Например механиката. Днес понеже имах големи пролуки в работата и обиколих доста места , за съжаление от към кеханика нещата хич не са добре , поне в Бг. Като цяло малко се ядосвам на хората обиждащи Бг из форумите, но сега ще се присъединя към тях. Как може на всяка бензиностанция да има по един диск на русия дебел ром азис , а в цял един град да не може да се намери свястна меаника.

Добре де, намира се тук -там , но е на някакви нереални цени- наспример мейнсторма на лего е 839 лева при цебна в ибея 249 долара. :lol: Разбирам , че този магазин комсед е огромен и все трябва да избие тоя наем , ама .... изобщо не допусках , че един представител на някоя марка в бг ще продава на 2 пъти по-високи цени от препоръчителната-тия хора нямат ли договори?????

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

Вие как се справяте със зъбните колела?

41
Форум на Web-мастъра / Относно обявите
« -: Януари 28, 2009, 09:49:55 pm »
Предлагам да се въведат няколко изисквания към пускащите обяви.

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

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

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

42
Глава 5: Свободна памет и презаредими имена

Съдържание на пета глава :

5.1. Разпределение на свободната памет
5.2. Един пример за свързан списък
5.3. Презаредими имена на функции
5.4. Указатели към функции
5.5. Свързване, безопасно относно типовете


Тази глава разглежда две фундаментални концепции - свободната памет за програмата и презареждането на имената на функциите. Свободната за програмата памет позволява да се отделя памет по време на изпълнение. Реализацията на класа IntArray в глава 1 ни предложи един кратък първи поглед върху този проблем. В тази глава ние ще го разгледаме подробно. Презареждането на имената на функциите позволява на няколко екземпляра на дадена функция, която предлага една обща операция, отнасяща се за аргументи от различен тип, да имат едно и също име. Ако вече сте написали поне един аритметичен израз на някой програмен език, то вие сте използвали предварително дефинирани презаредими функции. В тази глава ние ще видим как да дефинираме наши собствени такива функции.

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

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

IntArray
IntArray( int sz ) {
size = sz;
ia = new int[ size ];
for ( int i = 0; i < sz; ++i ) ia[ i ] = 0;
}

IntArray има даннови елемента size и ia. ia, който е указател към цяло число, ще адресира разположението на масива в свободната памет. Един от аспектите на използването на свободната памет е, че тя не е именувана. Обектите, разположени в тази памет, се обработват индиректно чрез указатели. Втори аспект на използването на свободната памет, е, че тя не е инициализирана и следователно винаги трябва да й бъде давана стойност преди употреба. Поради това е написан и цикъла for, чрез който на всеки елемент на ia се дава стойност 0. size, разбира се, съдържа размера на масива.

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

int *pi = new int;

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

IntArray *pia = new IntArray( 1024 );

отделя памет за обекта клас IntArray. Скобите, които са записани след името на класа, ако ги има, се явяват като аргументи на конструктора на класа. В този случай, pia се инициализира като указател към обект - клас IntArray с 1024 елемента. Ако скобите липсват, както в израза

IntAarray *pia2 = new IntArray;

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

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

#include <string.h>
char *copyStr ( const char *s ) {
char *ps = new char[ strlen(s) + 1 ];
strcpy( ps, s );
retunr ps;
}

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

IntArray *pia = new IntArray[ someSize ];

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

Отделянето на памет по време на изпълнение се нарича динамично разпределяне на паметта. Казваме, че масивът, адресиран чрез ia, е разположен динамично. Отделянето на памет за самия указател ia, обаче, се извършва по време на компилация - това е причината, поради която ia може да бъде именуван обект. Отделянето на памет по време на компилация се нарича статично разпределяне на паметта. Казваме, че указателят ia е разположен статично.

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

За обекти, разположени в свободната памет, се казва, че притежават динамичен период на активност. Паметта, отделена чрез използватнето на оператора new остава свързана с обекта докато не бъде освободена явно от програмиста. Явното освобождаване се осъществява чрез прилагането на оператора delete към указателя, който адресира динамичния обект. Нека разгледаме един пример.

IntArray grow() разширява масива, адресиран чрез ia, с половината от размера му. Първо, трябва да бъде отделена памет за един нов по-голям масив. След това трябва да се копират стойностите на стария масив, а допълнителните елементи трябва да се инициализират със стойност 0. Накрая, паметта, заета от старият масив, трябва да се освободи явно чрез прилагане на оператора delete.

void IntArray grow() {
int *oldia = ia;
int oldSize = size;
size += size/2 + 1;
ia = new int[ size ];// copy elements of old array into new
for ( int i = 0; i < oldSize; ++i ) ia[ i ] = oldia[ i ];
for ( ; i < size; ++i ) ia[ i ] = 0;
delete oldia;
}

oldia има локален период на активност; паметта, отделена за нея, се освобождава автоматично, когато изпълнението на функцията приключи. Това не се отнася, обаче, за адресирания от oldia масив. Неговият период на активност е от динамичен тип и продължава да съществува и извън границите на локалния обхват. Ако паметта, отделена за масива, адресиран чрез oldia, не бъде освободена явно, като се използват оператора delete, тя остава присъединена към програмата. delete oldia; освобождава, отделената за масива памет.

Когато изтриваме масив, който е класов обект, на оператора delete трябва да е известен размера му. Това е необходимо за да бъде извикан подходящия класов деструктор. Например, даден е следния масив

IntArray *pia = new IntArray[ size ];

Тогава операторът delete, приложен към pia изглежда така

delete [size] pia;

Операторът delete трябва да бъде прилаган само за паметта, която е била отделена чрез оператора new. Прилагането на оператора delete към памет, която не е отделена от свободната памет вероятно ще се прояви в не дефинираното поведение на програмата по време на изпълнение. Прилагането на оператора delete върху указател със стойност 0, обаче, не предизвиква никакви опасни последици - т.е. към указател, който не адресира обект. Следват няколко примера за безопасно и не безопасно прилагане на оператора delete

void f(){
int i;
char *str = "dwarves";
int *pi = &i; // dangerous delete pi;
intArray *pia = 0; // dangerous delete pia;
doduble *pd = new double;
delete str;
}

Свободната памет на програмата не е безкрайна; през времето на изпълнение на програмата тя може да бъде изчерпана. (Разбира се, ако обектите, които не са необходими повече, не бъдат изтрити, ще се увеличи скоростта на изчерпване на свободната памет). По подразбиране, new връща 0, когато наличната свободна памет не е достатъчна за да удовлетвори заявката.

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

ia = new int[ size ]; // trouble
if new returns 0
for ( int i = 0; i < oldSize; ++i )
ia[ i ] = oldia[ i ];

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

ia = new int[ size ];
if ( !ia ){ error("IntArray
grow()
free store exhausted");
}

където error() е една обща функция, дефинирана от програмиста и предназначена да съобщава за грешки като осигурява елегантен изход.

Ето една малка програма, която илюстрира използването на grow()

#include <stream.h>
#include "IntArray.h"

IntArray ia[ 10 ];
main() {
cout << "size " << ia.getSize() << "n";
for ( int i = 0; i < ia.getSize(); ++i )
ia = i*2; // initialize ia.grow();
cout << "new size " << ia.getSize() << "n";
for ( i = 0; i < ia.getSize(); ++i )
cout << ia << " ";
}

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

size 10
new size
16 0 2 4 6 7 10 12 14 16 18 0 0 0 0 0 0

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

#include <stream.h>

viod exhaustFreeStore( unsigned long chunk ) {
static int gepth = 1;
static int report = 0;
++depth; // keep track of invocations
double *ptr = new double[ chunk ];
if ( ptr )
exhaustFreeStore( chunk );// free store exhausted
delete ptr;
if ( !report++)
cout << "Free Store Exhausted" << "tchunk " << chunk
<< "depth " << depth << "n";
}

Четирикратното изпълнение на exhaustFreeStore() С аргументи, които имат различен размер дава следния резултат

Free Store Exhaused
ckunk 1000000 depth 4

Free Store Exhaused
ckunk 100000 depth 22

Free Store Exhaused
ckunk 10000 depth 209

Free Store Exhaused
ckunk 1000 depth 2072

Една от С++ библиотеките предлага известна помощ, като поддържа информация за свободната памет. Манипулаторът, обработващ изключенията _new_handler се разглежда в Раздел 5.4 по-нататък в тази глава.

Програмистът също може да постави обект, разположен в свободната памет на определен адрес. Формата на такова извикване на оператора new има вида

new (place_address) type-specifier

където place_address трябва да бъде указател. За да използвате оператора new по този начин трябва включите заглавния файл new.h. По този начин програмистът може да преразпределя паметта, която в един по-нататъшен момент ще съдържа обекти, определени чрез тази форма на оператора new. Например,

#include <stream.h>
#include <new.h>

const Chunk = 16;

class Foo { public
int val;
Foo() { val = 0; }
};// preallocate memory, but no Foo objects

char *buf = new char[ sizeof( Foo ) * Chunk ];
main() {
// construct Chunk Foo objects for buf
Foo *pb = new (buf) Foo[ Chunk ];

// check that objects were plased in buf
if ( (char*)pb == buf )
cout << "Operator new worked! pb "<< pb << " buf "
<< (void* )buf << "n";
}

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

Operator new worked!
pb 0x234cc
buf 0x234cc

Възможно е да се появи известно объркване относно тази програма. То е свързано с изпращането на buf към void*. Това е необходимо, понеже когато към оператора за изход се изпраща операнд char* се отпечатва "null terminated string", т.е. низа, който е адресиран. Чрез изпращането на buf към void* операторът за изход знае, че трябва да отпечата адресната стойност на buf. Това се дължи на факта, че операторът за изход се презарежда така, че да използват два различни указателни типове на аргументи char* и void*. Презаредимите функции се разглеждат в един от подразделите на тази глава. Въпреки, че този тип на оператора new се използва главно с типовете class, той може да се използват и за вградените типове данни. Например,

#include <new.h>

int *pi = new int;

main(){
int *pi2 = new (pi) int;
}


5.2. Един пример за свързан списък

В този раздел се реализира един елементарен клас списък от цели числа за да бъде илюстрирана както работата с указатели, така и използването на операторите new и delete. Като минимум IntList трябва да поддържа две стойности - цялата стойност на елемента на списъка и адреса на следващия елемент на списъка. Това може да се представи по следния начин

int val;
ListItem *next;

Един списък представлява последователност от елементи. Всеки елемент съдържа стойност и указател, може и null, към следващия елемент на списъка. Списъкът може да бъде и празен; т.е. да бъде списък без елементи

IntList i1; // the empty list

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

i1.insert( someValue );

или добавяни към края му

i1.append( someValue );

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

i1.remove( someValue );

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

i1.display();

Ето една първа програма, която бихме желали да напишем като използваме класа IntList.

#include "IntList.h"

const SZ = 12;

main() {
IntList i1;
i1.display();
for ( int i = 0; i < SZ; ++i )
i1.insert( i );
i1.display();
IntList i12;
for ( i = 0; i <SZ; ++i ) i12.append( i );
i12.display();
return 0;
}

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

( empty )( 11 10 9 8 7 6 5 4 3 2 1 0 )( 0 1 2 3 4 5 6 7 8 9 10 11 )

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

class IntList {
public IntList ( int = ??? );
// ...private
int val;
IntLIst *next;
};

При този проект възникват няколко проблема. Всичките те произтичат от объркването между обекта списък и елемента на списъка. Например, при този проект не се допуска наличието на празен списък. Не съществува начин за разграничаване на списъка, съдържащ един елемент от празния списък. Въпросителните знаци, в сигнатурата на конструктора на IntList са предназначени да подчертаят този проблем. Няма подразбираща се стойност за инициализиране на val, чрез която да се отбелязва, че списъкът е празен. Другите проблеми възникват от това, че не е определен смисъла на insert() и remove() когато обектът от тип IntList предлага също и първия елемент на списъка.

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

class IntList;
class intItem{
friend class IntList;
private IntItem( int v=0 ) { val = v; next = 0 )
IntItem *next;
int val;
};

IntItem се нарича клас private (личен). Само на IntList е разрешено да създава и обработва IntItem обектите. Това е смисъла на декларацията friend. Раздел 6.5 разглежда подробно тази декларация. Раздел 6.1 обяснява разликите между декларациите private и public. IntList е реализиран по следния начин:

class IntItem;
class IntList {
public IntList(int val) { list = new IntItem( val ); }

IntList() { list = 0; }// ...
private
IntItem *list;
};

Упражнение 5-1. Защо IntList се нуждае от два конструктора? Защо, например, да не дефинираме простоIntList( val = 0 );
Упражнение 5-2. Един допълнителен член данни на IntList може да бъде int len; // length of list, който да съдържа броя на елементите на списъка. Разгледайте аргументите за и против тази декларация.

Следващата стъпка се състои в реализирането на член-функции, които поддържат потребителските обработки на IntList обектите. insert() поставя даден нов IntItem в началото на списъка. Това се реализира така

IntList
insert( int val ) {
// add to the front of the list
IntItem *pt = new IntItem( val );
pt->next = list;
list = pt;
return val;
}

append() е малко по-сложна. Тя трябва да добавя нов IntItem в края на списъка. Една помощна функция, atEnd(), връща указател към последния елемент на списъка

IntItem *IntList
atEnd(){ // return pointer to last item on list
IntItem *prv, *pt;
for ( prv=pt=list; pt; prv=pt; pt=pt->next ); // null statement
return prv;
}

append() трябва да проверява специалния случай на празен списък. Реализацията изглежда по следния начин
IntList
append( int val ) {
// add to the back of the list
IntItem *pt = new IntItem( val );
if ( list == 0 ) list = pt;
else (atEnd())->next = pt;
return val;
}

Упражнение 5-3. Разгледайте аргументите за и против поддържането на следния IntList член.

IntItem *endList;

Как това може да се отрази на реализацията на append()?

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

#include <stream.h>

const int lineLength = 16;
IntList

display() { // display val member of list
if ( list == 0 ) {
cout << "( empty )n";
return 0;
}

cout << "( ";
int cnt = 0; // number of items displayed
IntItem *pt = list;
while ( pt ) {
if ( ++cnt % lineLength == 1 && cnt != 1 )
cout << "n ";
cout << pt->val << " "; pt = pt->next;
}

cout << ")n";
return cnt;
}

Проверката

if ( ++cnt % lineLength == 1&& cnt != 1 )

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

class IntList; // forward declaration
class IntItem {
friend class IntList;
private
IntItem(int v=0) { val = v; next = 0; }
IntItem *next;
int val;
};

class IntList {
public IntList(int val){ list = new IntItem( val );}
IntList() { list = 0; )
display();
insert( int = 0 );
append( int = 0 );
private
IntItem *atEnd();
IntItem *list;
};

Потребителят трябва да бъде в състояние да отстранява елементи от списъка или да изтрива целия списък. От проектанта на класа зависи дали опита за отстраняване на елемент от празен списък да се определя като грешка. Но и в двата случая на потребителя на класа е необходима функцията-предикат isEmpty()

class IntItem { /* ... */ ;
class IntList {
public isEmpty() { return list == 0; }
// ...private
IntItem *list;
);

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

IntList
remove() {
// delete the entire list
IntItem *tmp, *pt = list;
int cnt = 0;
while ( pt ) {
tmp = pt;
pt = pt->next;
++cnt;
delete tmp;
}

list = 0;
return cnt;
}

Съответно, потребителят може да иска да отстрани всички елементи, които съдържат определена стойност. Един особен случай при извършването на това, е случаят, когато трябва да бъде отстранен първия елемент на списъка. Тогава трябва да бъде изменен и самият член на list.
IntList
remove( int val ) {
// delete all enries with value val
IntItem *prv, *tmp, *pt = list;
int cnt = 0;
while ( pt & pt->val == val )
// while the first item on list == val {
tmp = pt->next; // save pointer to next
delete pt;
++cnt;
pt = tmp;
};

if ( (list = pt) == 0 ) return cnt; // list empty
prv = pt;
pt = pt->next;
while ( pt ) {
// iterate through list
if ( pt->val == val ) {
tmp = prv->next = pt->next;
delete pt;
++cnt;
pt = tmp;
}

else {
prv = pt;
pt = pt->next;
}

}; // end, while (pt)
return cnt;
}

Една особено полезна член функция e length(). length() връща броя на елементите на списъка. За празния списък, разбира се, трябва да бъде връщана стойност 0.

IntList
length() {
int cnt = 0;
IntItem *pt = list;
for ( ; pt; pt = pt->next, ++cnt );
// null statement
return cnt;
}

Ето една втора малка програма, която демонстрира тези четири член-функции. (разширената спецификацията на заглавния файл IntList.h, беше оставена като упражнение за читателя).

#include "IntList.h"
#include <stream.h>

const SZ = 12;
const ODD = 1;

main() {
IntList i1; // empty lilst
if ( i1.isEmpty() &&i1.length() == 0 &&i1.remove() == 0)
// test that empty list is handled
cout << "Empty List ok.n";
// every odd item is set to value of ODD
for ( int i = 0; i < SZ; ++i )
i1.append( i%2 == 0 ? i ODD );
i1.display(); // illustrate remove( someValue );
cout << i1.remove( ODD ) << " items of value "
<< ODD << " removed ";
i1.display();// illustrate remove()
int len = i1.length();
if ( i1.remove() == len )
cout << "All " << len << " items removed ";
i1.display();
return 0;
}

Когато компилираме и изпълним тази програма ще получим следните резултати

Empty List ok.

( 0 1 2 1 4 1 6 1 8 1 10 1 )
6 items of value 1 removed

( 0 2 4 6 8 10 )
All 6 items removed

( empty )

Упражнение 5-4. Реализирайте IntList removeFirst(). Нека стойността, която връща тази член-функция е стойността на члена val. Уверете се, че обработвате и случая на празен списък.

Упражнение 5-5. Реализирайте IntList removeLast(). Нека отново, стойността, който връща тази член-функция да бъде стойността на члена val. Уверете се, че обработвате и случая на празен списък.

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

#include "IntList.h"

void IntLIst
concat( IntList& i1 ) {
( atEnd() )->next = i1.list; }

Проблемът се състои в това, че два IntList обекта ще сочат една и съща последователност от елементи. Много е вероятно двата класови обекта да трият елементи по различно време в програмата. Ако вторият обект се опитва да получи достъп до елементи, които вече са изтрити, ще се появят висящи псевдоними (указатели), които вероятно ще причинят грешки по време на изпълнение на програмата. Ако това не се случи, съществува възможност вторият обект по-късно да се опита да изтрие елемент, чиято памет вече да се окаже отделена за някаква съвсем различна цел. Отново е съвсем вероятно програмата да бъде прекъсната по време на изпълнение. Едно общо решение е да се осигури брояч-псевдоним, за всеки елемент на списъка.

Всеки път, когато се отстранява елемент,броячът-псевдоним се намалява с 1. Когато той стане 0, елементът може да бъде изтрит фактически. Една алтернативна стратегия е да се копира всеки елемент, който участвува в обединението. Тази версия на concat() изглежда така

void IntList
concat( IntList& i1)

{ // append i1.list to invoking list object
IntItem *pt = i1.list;
while ( pt ) {
append( pt->val );
pt = pr->next;
} }

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

void IntList
reverse() {
IntItem *pt, *prv, *tmp;
prv = 0;
pt = list;
list = atEnd();
while ( pt != list ) {
tmp = pt->next;
pt->next = prv;
prv = pt;
pt = tmp;
}

list->next = prv;
}

Следната малка програма илюстрира concat() и reverse()

#include "IntList.h"
const SZ = 8;
main() {
IntLIst i1, i12;
for ( int i = 0; i < SZ/2; ++i ) i1.append( i );
for ( i = SZ/2; i < SZ; ++i ) i12.append( i );

i1.display();
i12.display();
i1.concat( i12 );
i1.display(); // concat
i1.reverse();
i1.display(); // reverse
return 0;
}

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

( 0 1 2 3 )
( 4 5 6 7 )
( 0 1 2 3 4 5 6 7 ) ( 7 6 5 4 3 2 1 0 )

Упражнение 5-6. Реализирайте член функция за добавяне на елемент в списъка, така че IntItem, който го следва да има стойност, която да е първата стойност в списъка, по-голяма от стойността на добавяния елемент.

Упражнение 5-7. Променете IntList, така че да притежава и елемент IntItem *endList; Когато изменяте public член функции се уверете, че не нарушавате нещо в съществуващия текст (трите примерни програми в този раздел).

5.3. Презаредими имена на функции

За дадена дума се казва, че е презаредима, ако има две или повече различни значения. Смисълът, в който е употребена думата, се определя в зависимост от контекста. Ако напишем

static int depth;

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

В естествените езици двусмислието често е умишлено. В литературата, например, двусмислието може да обогати нашето разбирането на героите и тематиката на книгите. Едно лице, може да бъде описано като ограничено (задължено, обвързано) и решително (непоколебимо, твърдо). Един от героите може да се обърне към друг и да каже "Хората никога не са справедливи (верни, точни)". Читателят може да възприеме различните значения на думата едновременно.

Двусмислието, обаче, е неподходящо за компилатора. Ако контекстът, в който се появява даден идентификатор или оператор не е достатъчен за да се изясни значението му компилаторът издава съобщение за грешка. Двусмислието, обаче, е особено важно при презаредимите имена на функции, темата на този раздел, както и на наследствеността при класовете, което е предмет на обсъждане на глави 7 и 8.Защо да презареждаме имената на функциите?

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

int max( int, int);
double max(double, double );
Complex &( const Complex, const Complex );

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

От потребителска гледна точка съществува само една операция, която определя максимална стойност. Детайлите по реализацията относно начина, по който това се извършва, се отнасят до един по-широк кръг интереси. Чрез презареждането на функциите потребителят може просто да напише следното:

int i = max( j, k );
Complex c = max( a, b );

На английски употребените думи са bound and determined. Изречението има вида

"People are never just".

Един аналог ни предлага аритметичният оператор. Изразът 1 + 3 извиква операцията събиране за цели операнди, докато израза 1.0 + 3.0 извиква различна операция за събиране, която обработва операнди с плаваща запетая.

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

Без способността за презареждане на име на функция на всеки нейн представител трябва да бъде дадено собствено уникално име. Например, нашето множество от max() функции ще придобие вида

int max( int, int );
double fmax( double, double );
Complex &Cmax( const Complex&, const Complex& );

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

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

Как да презаредим име на функция

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

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

// declares the same function
extern void print( int *ia, int sz );
void print( int *array, int size );

Имената на аргументите не са съществени за сравнението на сигнатурите.

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

unsigned int max( int*, int sz );
extern int max( int *ia, int ); // error

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

extern void print( int *, int );
void print(double *da, int sz );

Една декларация typedef предлага алтернативно име за съществуващ тип данни; то не създава нов тип данни. Следните два представителна search() се третират като притежаващи една и съща сигнатура. Декларацията на втория представител ще предизвика грешка по време на компилация понеже въпреки, че притежава същата сигнатура, тя има различен тип за връщане.

// typedef does not introduce a new type
typedef char *string;
extern int search( string );
extern char search( char ); // error


Кога да не използваме презареждането на функции ?

Механизмът на презареждането позволява множество от функции, които изпълняват сходна операция, такава като print(), да бъдат извиквани чрез едно общо мнемонично име. Свързването с подходящия представител на функцията е прозрачно за потребителя, като при това отстранява лексикалната сложност, породена от необходимостта на всяка функция да се дава уникално име, като iPrint() и iaPrint().

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

void setDate( Date&, int, int, int );
Date& convertDate( char* );
void printDate( const Date& );

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

class Date{
set( int, int, int );
Date &convert( char* );
void print();
// ...};

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

Screen& moveHome();
Screen& moveAbs( int, int );
Screen& moveRel( int, int, char *direction );
Screen& moveX( int );
Screen& moveY( int );

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

Screen& ( int, char xy );

Така получаваме уникална сигнатура. Освен това, ако някакво проучване покаже, че по оста x или y промените са по-чести, можем да зададем стойност по подразбиране

Screen& move( int, char xy = `xґ);

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

Screen& move( int sz = 1, char xy = `xґ );

Сега и двете функции move() и moveHome() могат да бъдат викани без аргументи. Не е необходимо аргумент с инициализатор по подразбиране да бъде разглеждан, когато се опитваме да съпоставим определен представител при извикване на презаредима функция.

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

inline Screen&Screen
home(){ return move( 0, 0 ); }

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

move( int, int, char* = 0 );

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

Свързване на обръщение към презаредима функция

Сигнатурата на функцията разграничава един представител от друг при набор от презаредими функции. Например, ето четири различни представителя на print()

extern void print( unsigned int );
extern void print( char* );
extern void print( char );
extern void print( int );

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

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

1. Успешно съпоставяне. Обръщението се свързва с подходящ представител. Например, например всяко от следните три обръщения към print() има като резултат съпоставяне

unsigned a;
print( `aґ ); // matches print(char);
print( "a" ); // matches print(char*);
print( a ); // matches print(unsigned);

2. Неуспешно съпоставяне. Фактическите аргументи не могат да бъдат поставени в съответствие с аргументите на дефинираните представители. Всяко от следните две обръщения към print() има като резултат неуспешно съпоставяне

int *ip;
SmallInt si; // error no match
print( si )
print( ip ); // error no match

3. Двусмислено съпоставяне. Фактическите аргументи могат да бъдат съпоставени с повече от един дефиниран представител. Следното обръщение е един пример за двусмислие при съпоставянето, понеже такова може да бъде осъществено с всеки от представителите на print(), като изключим този, който получава аргумент от тип char*.

unsigned long u1;
print( u1 ); // error ambiguous

Съпоставянето може да бъде извършено по един от следните три начина, в зависимост от приоритета:

1. Точно съпоставяне. Типът на фактическите аргументи съответства точно на типа на един от дефинираните представители. Например,

extern ff( int );
extern ff( char* );
f( 0 ); // matches
ff( int )0 е от тип int. Обръщението точно съответства на ff(int).

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

class X;
extern ff( X& );
extern ff( char* );
ff( 0 ); // matches
ff(char*)

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

class SmallInt
{ operator int();// ...
SmallInt si;
extern ff( char* );
extern ff( int );
ff( si ); // matches
ff(int);

operator int() се нарича оператор за преобразуване. Той позволява на класа да дефинира собствен набор от "стандартни" преобразувания. Раздел 7.5 разглежда подробно тези дефинирани от потребителя преобразувания.

Особености на точното съпоставяне

Фактическите аргументи от тип char, short и float се обработват като специален случай, като се спазва изискването за точно съпоставяне. Правят се два прегледа на набора от презаредими функции винаги когато съществува фактически аргумент за една от тях.

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

ff( char );
ff( long );
ff( `aґ ); // ff(char)

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

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

- аргументи от тип char, unsigned char или short се привеждат към тип int. Аргументи от тип unsigned short се привеждат към тип int ако машинния размер на int е по-голям от този на short; иначе се првеждат към тип unsigned int.

- аргументи от тип float се првеждат към тип double.

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

ff( int );
ff( short );
ff( long );
ff( `aґ );
// ff(int);

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

Един фактически аргумент от тип int не се съпоставя точно на формални аргументи от тип char или short. Съответно double не съответствува точно на аргумент от тип float. Например, дадена е следната двойка от презаредими функции,

ff( long );
ff( float );

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

ff( 3.14 ); // error ambiguous

Литералната константа е от тип double. Тя не съответства точно на нито един представител. С двата представителя се постига съпоставяне чрез прилагане на стандартните преобразувания. Понеже съществуват две възможни преобразувания обръщението се отбелязва като двусмислено. На нито едно стандартно преобразувание не се дава приоритет спрямо друго. Програмистът трябва да разреши проблема с двусмислието или чрез явно конвертиране, такова като

ff( long( 3.14 )); // ff(long)

или като използва суфикс за означаване на константа float

ff( 3.14F ); // ff(float)

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

ff( unsigned );
ff( int );
ff( char );

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

43
Глава 4: Функции и обхват.

Съдържание на четвърта глава :
4.1. Рекурсия
4.2. Функции inline
4.3. Строга проверка на типовете
4.4. Връщане на стойност
4.5. Списък от аргументи на функция
4.6. Изпращане на аргументи
4.7. Аргумент - псевдоним (reference)
4.8. Аргумент - масив
4.9. Програмен обхват
4.10. Локален обхват


За функциите може да се мисли като за потребителско дефинирани операции. Най-общо казано функцията се представя чрез име, а не чрез оператор. Операндите на функцията, наречени нейни аргументи, се задават от затворен в скоби списък от аргументи, разделени чрез запетаи. Резултатът на функцията се нарича нейн тип за връщане. Функция, която не връща стойност, има тип за връщане void.

Фактическите действия, които реализира една функция, са описани в тялото й. Тялото на функцията се затваря във фигурни скоби ("{}") и понякога се нарича блок. Ето няколко примера за функции

intline int abs( int i ) { // return the absolute value of i
return( i < 0 ? -i; i );
}

inline int min( int v1, int v2 ) { // return the smaller of two values
return ( v1 < v2 ? v1 ; v2 );
}

gcd( int v1, int v2 ) {
// return greatest common denominator
int temp;
while ( v2 ) {
temp = v2;
v2 = v1 % v2;
v1 = temp;
}
return v1;
}

Една функция се изпълнява, когато към името й се приложи операторът за извикване на функция ("()"). Ако функцията очаква да получи аргументи, тези аргументи, наречени фактически аргументи на извикването, се поставят в оператора за извикване на функция. Аргументите се отделят със запетаи. Това се нарича изпращане на аргументи на функция. В следващия пример main() извиква abs() два пъти, min() и gcd() по веднъж. Тя е описана във файла main.C.

#include <stream.h>
#include "localMath.h"

main(){
int i, j;
cout << "Value "; // get value from standart input
cin >> i;
cout << "Value ";
cin >> j;
cout << "nmin " << min( i, j ) << "n";
i = abs( i );
j = abs( i );
cout << "gcd " << gcd( i, j ) << "n";
}

При обръщение към функция се извършва едно от две възможни действия. Ако функцията е била декларирана като inline, по време на компилация в точката на обръщение се извършва заместване на обръщението с тялото на функцията; иначе функцията се извиква по време на изпълнение. Обръщението към функция предизвиква предаване на управлението на извиканата функция изпълнението на текущата активна функция се преустановява. Когато приключи изчислението на извиканата функция прекъснатата функция продължава изпълнението си от точката, непосредствено следваща повикването. Управлението на извикването на функции се осъществява с помощта на програмния стек, създаван по време на изпълнение.

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

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

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

За да бъде компилирана main. C функциите abs(), min() и gcd() трябва първо да бъдат декларирани; иначе всяко от повикванията им в тялото на main() ще предизвика грешка по време на компилация.

Трите прототипа имат вида (не е необходимо да се задават имената на имената на аргументите, а само типа им)

int abs( int );
int min( int, int );
int gcd( int, int );

Наи-добре е прототипите на функциите (и дефинициите на функциите online) да се поместват в заглавни файлове. В последствие тези заглавни файлове могат да бъдат включвани навсякъде, където те са необходими. По този начин всички файлове делят една обща декларация; ако тази декларация трябва да бъде променена се коригира само един файл.

Заглавният файл може да бъде наречен localMath.h. Той може да има следния вид (inline функциите са дефинирани в заглавния файл, а не в текстов файл на програмата)

int gcd( int, int );// inlines are placed within header file
inline abs( int i) {
return( i<0 ? -i ; i ); }
inline min( int v1, int v2) {
return( v1 <= v2 ? v1 ; v2 ); }

Компилацията на програмата се извършва по следния начин $ CC main. C gcd.C
След изпълнение на програмата се получават следните резултати

Value 15
Value 123
min 15
gcd 3

4.1. Рекурсия

Функция, която прави обръщение към себе си директно или индиректно, се нарича рекурсивна. Функцията gcd(), например, може да бъде написана отново като рекурсивна

rgcd( int v1, int v2 ) { if (v2 == 0 )return v1;
return rgcd( v2, v1%v2 ); }

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

Извикването

rgcd( 15, 123 );

връща стойност 3. В таблица 4.1 са описани последователните стъпки при изпълнението.

Последното извикване rgcd(3,0) удовлетворява условието за спиране. Тя връща най-големия общ делител - 3. Тази стойност става връщаната стойност на всички предишни обръщения. Казва се, че тази стойност се промъква нагоре.

v1
v2
return

15
123
rgcd(123, 15)

123
15
rgcd( 15, 3)

15
3
rgcd( 3, 0)

3
0
3



Таблица 4.1 Стъпки при изпълнение

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

Факториел на едно число се пресмята като произведение на последователността от числа от 1 до числото. Например, факториела на 5 е 120; т.е.1 * 2 * 3 * 4 * 5 = 120

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

unsigned longfactorial ( int val ) {
if ( val > 1 )
return val * factorial( val - 1);
return val;
}

В този случай условието за спиране е val да има стойност 1.

Упражнение 4-1. Напишете factorial() като итеративна функция.
Упражнение 4-2. Какво ще се случи ако условието за спиране има вида if ( val != 0 )
Упражнение 4-3. Как мислите, защо връщаната стойност на функцията е дефинирана от тип unsigned long, докато аргументът е от тип int?



4.2. Функции inline

Един въпрос, който все още не е зададен директно, е защо min() беше дефинирана като отделна функция. Причината не е в намаляването на обема на текста. Фактически трябва да се напише един символ повече, т.е.

min( i, j );
вместо
i < j ? i ; j;

Ползата от дефинирането й като функция се състои в следното:

- много по-лесно се чете извикването на функцията min() отколкото един аритметичен if, особено когато i и j са сложни изрази.
- много по-лесно се променя един представител на дадена функция, отколкото тристате й появи в текста на програмата. Например, ако сме решили да променим условието така

i <= j

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

- съществува единна семантика в програмата. За всяка проверка се гарантира еднотипна реализация.
- използуването на функция предполага цялостна проверка на типа на аргументите. Грешките, свързани с типа се откриват още по време на компилация.

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

int minVal = i <= j ? i ; j;
intVal1 = min( i, j );

Функциите inline предлагат едно решение. Всяка функция inline се разширява "на реда" в точката на повикването си.

intVal1 = min( i, j );

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

intVal1 = (i <= j) ? i ; j;

Извикването на функцията min() по време на изпълнение се отстранява.

Функцията min() се декларира като inline чрез ключовата дума inline в дефиницията. Трябва да отбележим, обаче, че спецификацията inline е само една препоръка за компилатора. Една рекурсивна функция, такава като gcd(), например, не може напълно да разшири inline (въпреки, че нейното първо повикване би могло да бъде разширено). Вероятно функция с дължина 1200 реда няма да бъде разширена inline. Изобщо механизмът inline е средство за оптимизиране на малки няколко редови често извиквани функции.

4.3. Строга проверка на типовете

Функцията gcd() очаква два аргумента от тип int. Какво ще се случи ако й бъдат подадени аргументи от тип float или char*? Какво ще се случи ако се изпрати само един аргумент или повече от два?

Основните операции, които gcd() изпълнява над двата си аргумента са от модулната аритметика. Модулната аритметика не може да се прилага за не цели операнди. Следователно обръщението

gcd( 3.14, 6.29 );

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

gcd( "hello", "world" );

Или от случайното слепване на двете стойности в това обръщение?

gcd( 24312 );

Единственият желателен резултат от опита за компилиране на по-ранните две обръщения за gcd() е да бъде предизвикана грешка по време на компилация; не се препоръчва какъвто и да е опит за изпълнението им. В С++ тези две обръщения наистина водят до съобщения за грешки по време на компилация, които най-общо имат следната форма

gcd( "hello", "world" ); //error invalid argument
//types (char*, char*)// -expecting (int, int)
gcd( 24312 );//error missing value for argument two

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

gcd( 3, 6 );
и връща стойност 3.

С++ е строго типизиран език. По време на компилация се прави проверка за съответствието на типовете както на списъка от аргументи, така и на типа на резултата на извиканата функция. Ако бъде открито несъответствие между фактическите типове и типовете, декларирани в прототипа на функцията, ще бъде приложено неявно конвертиране ако това е възможно. Ако не е възможно неявно конвертиране или е неправилен броя на аргументите ще се получи грешка по време на компилация. Прототипът на функцията предлага на компилатора информация за типовете, която му е необходима при проверката на типовете. Това е причината една функция да не може да бъде викана преди да бъде декларирана.(ў)

4.4. Връщане на стойност

Типът на връщаната стойност на една функция може да бъде предварително дефиниран, дефиниран от потребителя или производен тип (такъв като указателен или съотнасящ тип). Следват няколко примера за такива типове

double sqrt( double );
char strcpy( char, const char* );
IntArray &Intarray qsort();
TreeNode *TreeNode inOrder();
void error(const char* ... );

Изключение представляват масивите и функциите, понеже те не могат да бъдат декларирани като типове за връщане на функция. Указател към масив или указател към функция, обаче, могат да бъдат декларирани като такива типове. Функция, която не връща стойност, трябва да бъде декларирана от тип void. Функция, за която явно не е дефиниран типа на връщаната стойност, по подразбиране се приема от тип int.

Следните две декларации на isEqual() са еквивалентни; и двете описват типа на връщаната от функцията стойност като int

int isEqual( long*, long* );
isEqual( long*, long* );

Операторът return прекратява изпълнението на текущата функция. Управлението на програмата буквално се връща към функцията, от която е била извикана току-що приключилата изпълнението си функция. Възможни са две форми на оператора return

return;
return expression;

Тази способност за проверка на типовете се счита за особено ценна, така че комисията ANSI за езика С е възприела прототипа на функция от С++ за езика ANSI С.

Операторът return не задължителен за функции, които са декларирани от тип void. Използува се обикновено за да предизвика прекратяване на изпълнението на функцията. (Този вид използване на оператора return съответствува на използването на оператора break в циклите). Едно неявно изпълнение на return се получава при достигане на последния оператор на функцията. Например,

void dCopy( double *scr, double *dst, int sz ) {
// copy scr array into dst
// simplifying assumption arrays are same size

if ( scr == 0 || dst == 0 ) // if either array is empty, quit
return;
if ( scr == dst ) // no need copy
return;
if( sz <= 0 ) // nothing to copy
return;
// still here@ copy.
for ( int i = 0; i < sz; ++i )
dst[ i ] = scr[ i ];
// no explicit return necessary
}

Втората форма на оператора return определя резултата на функцията. Той може да бъде произволен сложен израз; може да съдържа и обръщение към функция. Реализацията на функцията factorial(), например, съдържа следния оператор return

return val * factorial( val-1 );

Ако фактическата стойност, която се връща, не съответствува точно на типа за връщане, се прилага неявно конвертиране ако е възможно. Може да се каже, че по исторически причини не се счита за грешка факта, че една функция не декларира явно типа void, когато няма да връща стойност. Обаче, обикновено ще се появи предупреждение. main() е хубав пример за функция, която програмистът обикновено описва без оператор return. Програмистът трябва да бъде внимателен и непременно да добавя стойност за връщане във всяка точка на прекъсване на функцията. Например,

enum Boolean { FALSE, TRUE };
Boolean isEqual ( char *s1, char *s2 ) {
// if either are null, not equla

if ( s1 == 0 || s2 == 0 ) return FALSE; // if s1 == s2, return
// TRUE; else FALSE

if ( s1 == s2 ) // the same string
return TRUE;
while ( *s1 == *s2++ )

if (*s1++ == `�ґ ) return TRUE;
// still here not equal
return FALSE;
}

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

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

- може да бъде върнат събирателен тип данни, който съдържа множество от стойности. За този тип използуване класовете са по-гъвкави от масивите. Освен това, програмистът може да върне само указател към масив; той може също да върне обект от тип клас, указател или псевдоним на клас.

- формалните аргументи могат да бъдат дефинирани като указателни или съответстващи типове. Те могат да бъдат дефинирани така, че да съдържат самите стойности. (Този метод се разглежда в раздел 4.6 по-нататък в тази глава).

4.5. Списък от аргументи на функция

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

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

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

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

Пропускането на аргумент или изпращането на аргумент от неправилен тип са източници на сериозни грешки по време на изпълнение на програма, написана на предишния ANSI C език. Със въвеждането на строгата проверка на типовете, тези интерфейсни грешки почти винаги се откриват по време на компилация. Вероятността за възникване на грешка при изпращане на аргументите се увеличава с увеличаване на размера на списъка от аргументи - някои функции на FORTRAN приемат до 32 аргумента. Като едно общо правило може да се приеме, че броят на аргументите не трябва да бъде повече от осем. Ако една функция се нуждае от повече аргументи то вероятно тя се опитва да направи прекалено много неща; един по-добър проект би могъл да я раздели на две или повече по-специализирани функции.

Като една алтернатива на големия списък от аргументи програмистът може да дефинира класов тип, който да съдържа стойностите на аргументите. Полезността на това се изразява в следното:

1. Значително намалява сложността на списъка от аргументи.
2. Проверките за валидност на стойностите за аргументите могат да бъдат изпълнявани от членовете-функции на класа, а не вътре във функцията. Това намалява размера на функцията и я прави по-лесна за разбиране.

Синтаксис на списъка от аргументи

Списъкът от аргументи на функция не може да бъде пропускан. Функция, която не получава аргументи може да се опише или с празен списък от аргументи или със списък, съдържащ единствено ключовата дума void. Например, следните две декларации на fork() са еквивалентни

// equivalent declarations
int fork();
int fork( void );

Списъкът от аргументи се нарича сигнатура на функцията, защото често се използува за разграничаване на един представител на функцията от друг. Името и сигнатурата на една функция я идентифицират по уникален начин (Раздел 5.3, който се отнася за презарежането на функции обсъжда тази идея по-подробно).

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

min( int v1, v2 ); // error
min( int v1, int v2 ); // ok

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

print( int *array, int size );

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

Специалната сигнатура многоточие ...

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

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

foo( arg_list, ... );
foo( ... );

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

Функцията printf() от стандартната изходна библиотека на С е пример за това, кога е необходимо многоточието. printf() винаги получава символен низ като първи аргумент. Дали тя ще получи и други аргументи се определя от първият й аргумент, наречен форматиращ низ. Метасимволите, зададени чрез %, показват че съществуват и допълнителни аргументи. Например,

printf( "hello, worldn" );

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

printf( "hello, %sn", userName );

получава два аргумента. Символът % показва, че съществува и втори аргумент, а s показва, че типа на аргумента е низ. printf() е декларирана в С++ по следния начин

printf( const char* ... );

Според това описание при всяко извикване на printf() трябва да бъде изпратен един аргумент от тип char*. След това могат да бадат подавани каквито и да е аргументи.Следните две декларации не са еквивалентни

void f();
void f( ... );

В първият случай f() е декларирана като функция, която няма аргументи;
във втория - като функция с нула или повече аргументи. Обръщенията

f( someValue );
f( cnt, a, b, c );

са правилни само за втората декларация. Обръщението f(); е правилно и за двете функции.
Специалната сигнатура инициализация по подразбиране

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

За потребителя на операционната система UNIX, например, всеки текстов файл, създаден от него, се дефинира по подразбиране с разрешение за четене и запис за него и само за четене - за останалите. Ако ние желаем да разширим или стесним правата на достъп до файла, като системата UNIX поддържа прост механизъм за модифициране или заменяне на подразбиращите се стойности. Дадена функция може да определя подразбиращи се стойности за един или повече от аргументите си използувайки синтаксиса на сигнатурата. Функция, която създава и инициализира двумерен символен масив за да симулира екран на терминал може да зададе стойности по подразбиране за височината, ширината и вида на основата за символите на екрана

char * screenInit( int height = 42, int width = 80,
char background = ` `);

Функция, която предлага стойности по подразбиране за аргументите си може да бъде викана със или без съответните фактически аргументи. Ако е подаден аргумент той припокрива стойността по подразбиране; иначе се използува подразбиращата се стойност. Правилно е всяко от следните обръщения към screnInit()

char *cursor;
// equivalent to screenInit(24, 80,ґcursor = screenInit()`);
// equivalent to screenInit(66, 80,ґ cursor = screenInit( 66 )`);
// equivalent to screenInit(66, 256,ґ cursor = screenInit( 66, 256)`);
cursor = screenInit( 66, 256, `#ґ);

Забележете, че не е възможно да зададете стойност на background без да определите height и width. Такова свързване на аргументите се нарича позиционно. Част от работата по проектирането на една функция се състои в това да бъдат подредени аргументите в сигнатурата така, че стойността, която е най-вероятно да бъде инициализирана от потребителя да се намира на първо място. Допускането при проектирането на screenInit() (достигнато вероятно на основата на експерименти) е, че height е тази стойност, която най-вероятно ще бъде задавана от потребителя.

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

ff( int = 0 ); // in ff.h
#include "ff.h";
ff( int i = 0 ); { ... } // error

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

Успешната декларация на една функция може да определи допълнителни подразбиращи се инициализатори - това е един полезен метод за пригодяване на една обща функция към специфично приложение. Функцията chmod() от системната UNIX библиотека променя защитата на даден файл. Прототипът на функцията се намира в системния заглавен файл stdlib.h. Той е деклариран по следния начин

chmod( char *filePath, int protMode );

където protMode определя режима на защита на файл, а filePath представя името и пътя до местоположението на файла. Някакво частно приложение винаги променя режима на защита на файловете си на read-only. За да не се указва това всеки път chmod() се декларира повторно за да поддържа стойност по подразбиране

#include <stdlib.h>
chmod( char *filePath, int protMode = 0444 );

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

ff( int a, int b = 0, int c ); // ff.h

Как можем да декларираме отново ff() в някакъв наш файл, така че b да има подразбираш се инициализатор? Написаното по-долу е правилното представя подразбираш се инициализатор

#include "ff.h"

ff( int a, int b = 0, int c); // ok

За тази повторна декларация на ff() b е най-десният аргумент без подразбиращ се инициализатор. Следователно, правилото, че инициализаторът на стойност по подразбиране се присвоява позиционно, започвайки от най-десния аргумент, не е нарушено. Фактически, сега бихме могли да дефинираме ff() за трети път

#include "ff.h"
ff( int a, int b = 0, int c);
// ok
ff( int a = 0, int b, int c);
// ok

4.6. Изпращане на аргументи

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

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

Подразбиращият се в С++ метод за инициализация при изпращането на аргументи е чрез копиране на стойностите за четене (rvalue) на фактическите аргументи в областта, отделена за формалните аргументи. Това се нарича изпращане по стойност. При изпращането по стойност функцията никога няма достъп до фактическите аргументи на обръщението. Стойностите, които функцията обработва са нейни собствени локални копия; те са записани в стека. Изобщо, промените направени над тези стойности не се отразяват на стойностите на фактическите аргументи. Когато функцията приключи работата си и записа на активирането бъде изтрит тези локални стойности се изгубват. При изпращането по стойност съдържанието на фактическите аргументи не се променя. Това означава, че програмистът не е длъжен да запазва и възстановява стойностите на аргументите, когато прави обръщение към функция. Без механизма за изпращане по стойност може да се предполага, че всеки формален аргумент, който не е деклариран от тип const може да бъде потенциално изменен при всяко извикване на функцията. Извикването по стойност има минимален потенциал за нанасяне на щети и изисква минимум усилия от потребителя. Изпращането по стойност е един разумен механизъм за изпращане на аргументи по подразбиране.

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

- когато като аргумент трябва да бъде изпратен голям обект от тип клас. Времето и паметта, необходими за копиране и разполагане на класовия обект в стека често може да се окажат много големи за програмни приложения от реалната практика.

- когато стойностите на аргументите трябва да бъдат обработени. Функцията swap() е един пример, при който потребителят иска да промени стойностите на фактическите аргументи, но това не може да бъде направено чрез механизма за изпращане по стойност.

void swap( int v1, int v2) {
int tmp = v2;
v2 = v1;
v1 = tmp; }

swap() разменя локалните копия на аргументите си. Фактическите променливи, изпратени на swap(), остават непроменени. Това се илюстрира от следната програма, която вика swap()

#include <stream.h>

vpid swap( int, int);
main() {
int i = 10;
int j = 20;
cout << "Before swap()ti "<< i << "tj" << j << "n";
swap( i, j );
cout << "After swap()ti "<< i << "tj" << j << "n"; }

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

Before swap()
i 10 j 20

After swap()
i 10 j 20

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

void pswap( int *v1, int *v2) {
int tmp = *v2;
*v2 = *v1;
*v1 = tmp; }

main() трябва да бъде модифицирана така, че да декларира и извиква pswap(). Програмистът вече трябва да изпраща адресите на двата обекта, а не самите обекти

pswap( &i, &j );

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

Before swap()
i 10 j 20

After swap()
i 20 j 10

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

void print( const BigClassObject* );

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

void rswap( int &v1, int &v2 ); {
int tmp = v2;
v2 = v1;
v1 = v2; }

Обръщението към rswap() от main() изглежда така, както и обръщението към оригинала swap()

rswap( i, j );

След като тази програма бъде компилирана и изпълнена ще се види, че стойностите на i и j са правилно разменени.

4.7. Аргумент - псевдоним (reference)

Този аргумент изпраща на функцията стойността за запис на фактическия аргумент. Използуването му има следните ефекти:

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

Ако аргументът от псевдонимен тип не се променя във функцията се препоръчва да бъде деклариран като const. Забележете, обаче, че е неправилно декларирането на х като const в следния пример

class X;
int foo( X& );
int bar( const X& x ) {
// const passed to nonconst reference
return foo ( x ); // error }

x не може да бъде изпратен като аргумент на foo() освен ако сигнатурата на foo() не бъде променена на const X& или X. Псевдонимен аргумент от компактен тип или от тип с плаваща запетая може да се държи неочаквано, когато фактическият аргумент не му съответствува точно по тип. Това се дължи на факта, че се генерира временен обект, на който се присвоява стойността за четене на фактическия аргумент, и тогава този обект се изпраща на функцията. Например, ето какво ще се случи, когато извикате rswap() с аргумент unsigned int

int i = 10;
unsigned int ui = 20;
rswap( i, ui );

Това обръщение се интерпретира така

int T2 = int(ui);
rswap( i, T2 );

Изпълнението на това обръщение към rswap() дава следния неправилен резултат

Before swap()
i 10 j 20

After swap()
i 20 j 20

ui остава непроменена, понеже тя никога не се изпраща на rswap(). По-скоро се изпраща T2 - временно генерирания обект поради несъответствието на типовете. В резултат се моделира изпращането по стойност. (Компилаторът трябва да издаде предупреждение).

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

void prswap( int *&v1, int *v2 ) {
int *tmp = v2;
v2 = v1;
v1 = tmp; }

Декларцията int *&p1; трябва да бъде четена от ляво на дясно. p1 е псевдоним на указател към обект от тип int. Променената реализация на main() ще изглежда така

#include <stream.h>
void prswap( int *v1, int *&v2 );

main() {
int i = 10;
int j = 20;
int *pi = &i;
int *pj = &j;
cout << "Before swap()tpi "<< *pi << "tpj" << *pj << "n";
prswap( pi, pj );
cout << "After swap()tpi "<< *pi << "tpj" << *pj << "n";
}

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

Before swap()
i 10 j 20

After swap()
i 20 j 10

По подразбиране връщаният тип също се изпраща по стойност. За големи класови обекти псевдонимният или указателният тип за връщане е по-ефективен; самият обект не се копира.

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

int IntArray
operator[]( int index ) {
return ia[ index ]; }
Intarray myAarray[ 8 ];
myArray[ 2 ] = myArray[ 1 ] + myArray[ 0 ];

Раздел 2.8 съдържа дефиницията на класа IntArray.

4.8. Аргумент - масив

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

void putValues( int[ 10 ] )]

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

void putValues( int* );

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

// three equivalent declarations of putValues
void putValues( int* );
void putValues( int[] );
void putValues( int[ 10 ] );

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

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

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

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

void putValues( int[ 10 ] ); // treated as int*
main() {
int i, j[ 2 ];
putValues( &i ); // ok
int*;
run-time error putValues( j ); // ok
int*;
run-time error return 0;
}

Проверката на типа на аргумента просто потвърждава, че двете обръщения към putValues() са с аргумент от тип int*.

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

void petValues9 ( int[], int size );

main() {
int i, j[ 2 ];
putValeus( i, 1 );
putValues( j, 2 );
return 0;}
putValues() отпечатва стойностите на масива в следния формат

( 10 ) &

44
Глава 3: Изрази и оператори.

Съдържание на трета глава :

3.1. Какво представлява изразът?
3.2. Аритметични операции
3.3. Операции за равенство и отношение, логически операции
3.4. Оператор за присвояване
3.5. Оператори за увеличаване и намаляване с 1
3.6. Операторът sizeof
3.7. Аритметичен оператор if
3.8. Оператори за работа с битове
3.9. Приоритет
3.10. Преобразуване на типове
3.11. Оператори
3.12. Оператори за управление
3.14. Операторът switch
3.15. Операторът while
3.16. Операторът for
3.17. Операторът do
3.18. Операторът break
3.19. Операторът continue
3.20. Операторът goto


Типовете данни бяха разгледани в глава 2. Ние описахме предварително дефинираните типове данни, методите за дефиниране на нови типове данни, както и как да създаваме даннови обекти. В тази глава се разглеждат предварително дефинираните операции, които могат да бъдат използувани за обработване на данните. Тези операции включват набор от оператори, които ги организират и ръководят изпълнението на програмата. В глава 4 ще бъдат обсъдени функциите и механизма за дефиниране на потребителски набор от операции.

3.1. Какво представлява изразът?

Всеки израз обединява една или повече операции. Обектите, над които се прилагат операциите се наричат операнди. Операциите се записват чрез оператори. Например, В С++ проверката за равенство се осъществява от оператора "= =". Операторите, които имат един операнд, се наричат унарни, докато операторите с два аргумента са бинарни. Операндите на бинарните оператори се делят на леви и десни. Някои оператори представят както унарни, така и бинарни операции. Например,

*ptr

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

var1 * var2

представя бинарния оператор умножение. Той изчислява произведението на операндите си var1 и var2.

За изчисляването на един израз се изпълнява една или повече операции за да бъде даден резултат. Изключвайки случая, когато е указано обратното, резултатът, връщан от израза е стойност за четене (rvalue). Типът на резултата на един израз се определя от типа на операндите му. Когато операндите имат различен тип се извършва преобразуване на типовете съобразно предварително дефиниран набор от правила. Раздел 3.10 разглежда подробно преобразуването на типовете.

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

Най-простата форма на израз се получава от единична литерална константа или променлива.
Т.е. "операнд" без операция. Резултатът е стойността за четене на операнда. Например,

3.14159

"melancholia" upperBound

Резултатът на 3.14159 е 3.14159. Типът му е double. Резултатът на "melancholia" e адреса в паметта на първия елемент на низа. Типът му е char*. Резултатът на upperBound е нейната стойност за четене (rvalue). Типът му се определя от дефиницията на променливата.

Следващите раздели разглеждат предварително дефинираните в С++ оператори, представени в удобен за изучаването им ред.


3.2. Аритметични операции

Операция
Функция
Използуване

Умножение
Expr * expr
/
деление
expr / expr

%
деление по модул
expr % expr

Събиране
Expr + expr

изваждане
Expr - expr


Таблица 3.1 Аритметични операции

Делението на цели числа дава цяло число. Ако частното има дробна част тя се отрязва. Например, и двата израза

21 / 6;
21 / 7;

дават като резултат 3.

Оператoрът за деление по модул ("%") изчислява остатъка от делението на две стойности. Той може да се използува само с операнди от цял тип. Левият операнд на операторът деление по модул е делимото. Делителят е десният операнд на операцията. Двата операнда трябва да са от цял тип. Следват няколко примера на правилни и неправилни изрази, използуващи оператора за деление по модул:

3.14 % 3 // error: floating point operand
21 % 6 // ok: result is 3
21 % 7 // ok: result is 0

int i;
double f;

i % 2 // ok: non-zero result indicates i is odd
i % f // ok: floating point operand

В някои случаи изчисляването на аритметичен израз ще върне неправилен или недефиниран резултат. Това се отнася за аритметични изрази. Може да се дължи на самото естество на математическата операция - деление на нула, например, - или да зависи от компютъра - препълване. Например, в променлива от тип unsigned char могат да се записват стойности от 0 до 255. При следната операция за умножение на променливата от тип unsigned char се дава стойност 256.

unsigned char uc = 32;
int i = 8;
uc = i * uc; // overflow

За да се представи 256 са необходими 9 бита. Присвояването на 256 на uc предизвиква препълване на паметта, отделена за този тип данни. Истинската стойност, записана на мястото на uc, е недефинирана и ще бъде различна за различните машини.

3.3. Операции за равенство и отношение, логически операции

В следствие на изпълнението на тези операции се получава стойност истина или лъжа. Условието, което има стойност истина, връща 1, а това с лъжа - 0.

Логическата операция AND ("&&") връща стойност истина само ако и двата й операнда се остойностяват като истина. Логическата операция OR ("||") връща стойност истина когато един от двата й операнда имат стойност истина. Операндите се изчисляват от ляво на дясно. Изчислението се прекратява когато стойността на израза може да бъде определена. В изразите:

expr1 && expr2

expr1 || expr2

expr2 не се изчислява съответно:

при логическата операция AND, ако expr1 има стойност лъжа; при логическата операция OR, ако expr1 има стойност истина.

операция
функция
използуване

!
логическо не
!expr1

<
по-малко
expr1 < expr2

<=
по-малко или равно
expr1 <= expr2

>
по-голямо
expr1 > expr2

>=
по-голямо или равно
expr1 >= expr2

==
равенство
expr1 == expr2

!=
различно
expr1 != expr2

&&
логическо и
expr1 && expr2

||
логическо или
expr1 || expr2


Таблица 3.2. Операции за равенство и отношение, логически операции

Ползата от такова изчисление на операцията AND се вижда, когато expr1 задава такова условие в израза, което ако има стойност лъжа, може да направи пресмятането на expr2 опасно. Например,

while ( ptr != 0 && ptr ->
value < upperBound && notFound( ia[ ptr -> value ] ))

Указател със стойност 0 не адресира никакъв обект. Прилагането на оператора за избор на елемент към указател със стойност 0 причинява винаги неприятности. Първият операнд на израза с AND предотвратява тази възможност. Излизането на индекса извън границите на масива е също така неприятно. Вторият операнд е предвиден да предотвратява тази възможност. Когато първите два операнда върнат стойност истина третият операнд може спокойно да бъде изчислен.

Логическият оператор NOT ("!") получава стойност истина когато неговият операнд има стойност 0; иначе има стойност лъжа. Например:

int found = 0;
while ( !found ) {
found = lookup( *ptr++ );
if ( ptr == endptr ) // at end
return 0; }

Изразът ( !found ) има стойност истина докато found има стойност 0.

Използуването на оператора NOT се свързва с въпроса за стила на програмиране. Например, значението на израза ( !found ) е ясно: не е намерен. Така ясно ли е, обаче, какво означава следното условие?

!strcmp( string1, string2 )

strcmp() е една вградена функция в С-библиотека, която сравнява два аргумента от тип низ за равенство. Ако върнатата стойност е 0, то двата низа са равни. Тогава изразът

!strcmp( string1, string2 )

означава следното: ако string1 не е равен на string2. В този случай използуването на оператора NOT би могло да доведе до неясноста на условието.


3.4. Оператор за присвояване

Левият операнд на оператора за присвояване ("=") трябва да бъде стойност за запис (lvalue). Резултатът от изпълнението на този оператор е записването на нова стойност в паметта, отделена на операнда. Например, нека са дадени следните три дефиниции:

int i;
int *ip;
int ia[ 4 ]; за които са валидни следните оператори:
ip * &i;
i = ia[ 0 ] + 1;
ia[ *ip ] = 1024;
*ip = i * 2 + ia[ i ];

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

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

main() {
int i, j;
i = j = 0; // ok: each assigned 0
// ... }

на i и j се дава стойност 0. Редът за пресмятане е от ляво на дясно.

Обединяването на операторите за присвояване позволява един по-компактен запис, като е при написания отново конструктор на IntArray, например ( вж. Раздел 2.8 за неговата оригинална реализация):

IntArray::IntArray( int sz ) {
ia = new int[ size = sz ];
// ... }

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

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

int arraySum( int ia[], int sz ) {
int sum = 0;
for ( int i = 0; i < sz; ++i )
usm += ia[ i ];
return sum; }

Обобщената синтактична форма на смесения оператор за присвояване има вида:

a op = b;

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

+=, -=, *=, /=, %=, <<=, >>=, &=, ^= , |=.

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

a = a op b;

Дългият запис на оператора за събиране, например, има вида:

sum = sum + ia[ i ];

Упражнение 3-1. Следният запис е правилен. Защо? Как бихте могли да го промените?

main()

{ int i = j = k = 0; }

Упражнение 3-2. Следният запис също е правилен. Защо? Как бихте могли да го промените?

main() {
int i, j;
int *ip;
i = j = 0; }

.5. Оператори за увеличаване и намаляване с 1

Операторите за увеличаване ("++") и намаляване ("-") с единица предлагат един удобен, компактен запис за добавяне и изваждане на 1 от променлива. За двата оператора може да се мисли като за оператори за присвояване; операндите трябва да бъдат стойности за запис (lvalue). Всеки оператор има както префксна, така и постфиксна форма. Например,

main() {
int c; ++c; // prefix increment
c++; // postfix increment }

Нека да илюстрираме използването на пост- и префиксната форма на операторите като дефинираме стеков клас IntStack, който поддържа цели стойности. За IntStack са дефинирани две операции:

1. push(int v), която поставя стойността на v на върха на стека.
2. pop(), която връща стойността, записана на върха на стека. Освен това възможни са два особени случая:

1. препълване: опит за запис в стека, когато е пълен.
2. изпразване: опит за четене от стека, когато той е празен.

IntStack ще бъде представен чрез един масив от цели числа. Следователно, необходими са следните членове (елементи):

int size;
int *ia;
int top;

top винаги ще съдържа индекса на елемента на върха на стека. Това означава, че за празен стек top има стойност -1. Стекът е пълен когато top има стойност size-1.

Написването на функциите isEmpty() и isFull(), които връщат стойности истина или лъжа е елементарно:

typedef int Boolean;
extern const int BOS; // Bottom of Stack
Boolean IntStack::isEmpty() { return (top == BOS);}
Boolean IntStack::isFull() { return (top == size-1);}

Реализацията на push() илюстрира префиксната форма на оператора за увеличение с 1. Да припомним, че top сочи текущия връх на стека. Новият елемент трябва да бъде поставен в елемент с едно по-голям от текущата стойност на top:

void IntStack::push( int v ) { // add v to the top of stack
if ( isFull() ) grow(); // enlarge stack
ia[ ++top ] = v; }

Префиксната форма на ++ увеличава стойността на top преди тази стойност да бъде използувана като индекс в ia. Това е компактния запис на следните два оператора:

top = top + 1;
ia[ top ] = v;

grow() увеличава размера на стека с някакъв предварително зададен брой елементи. Раздел 5.1 съдържа реализацията на grow().

Реализирането на pop() илюстрира постфиксната форма на оператора за намаляване с 1. Припомняме отново, че top сочи текущия връх на стека. След като бъде извлечен елемент от стека, top трябва да бъде намален с 1.

int IntStack::pop() {
// return the topmost element of stack
if( isEmpty() ) // report error and exit() ;
return ia[ top-- ]; }

Постфиксната форма на-намалява стойността на top след като тази стойност е била използувана като индекс на ia. Това е компактен запис на следната двойка оператори:

ia[ top ];
top = top - 1;

След намаляването на top се изпълнява оператора return.

Това,което остава за цялостното дефиниране на IntStack, е управлението на стека му. Понеже вътрешното представяне на нашия стеков клас е същото както и на дефинирания по-рано клас IntArray, ние ще използуваме реализацията на IntArray като базова за IntStack (вж. Раздел 2.8 ) за дефиницията на класа IntArray):

#include "IntArray.h"
typedef int Boolean;
const int BOS = -1; // Bottom of Stack
class IntStack :
private IntArray
public: IntStack( int sz = ARRAY_SIZE ) : IntArray( sz ),
top( BOS ) {} Boolean isEmpty();
Boolean isFull();
void push( int );
int pop();
void grow() {}
protected: int top;
}

При описанието на IntArrayRC, IntArray беше деклариран като базов клас public. При описанието на IntStack, IntArray е деклариран като базов клас private. Първата разлика между базовите класове private и public е, че на базов клас private не могат да бъдат присвоявани обекти от произлезлия клас. Например,

extern swap( IntArray&, int, int );
IntArray ia; IntArrayRC ia1;
IntStack is; ia = ia1; // ok: public base class
ia = is; // error: private base class
swap( ia1, 4, 7 ); // ok: public base class
swap( is, 4, 7 ); // error: private base class

Втората разлика между базовите класове private и public е, че наследените членове на базовия клас private се разглеждат като собствени членове на произлезлия клас. Това означава, че интерфейсът public на класа IntArray не е достъпен през класовите обекти на IntStack. Например,

int bottom = is[0]; // error: operator[] private

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

1. Като метод за създаване на подтип на съществуващ клас, както е при дефинирането на IntArrayRC, произлязъл от класа IntArray, описан в глава 1.

2. Като метод за повторно използуване на дадена реализация с цел създаване на нов тип клас, както е при дефинирането на IntStack, произлязъл от класа IntArray.

IntStack не е подтип на IntArray. Той не разделя операциите, които могат да бъдат прилагани към обектите на класа IntArray. Чрез определянето на IntArray като базов клас private на IntStack не се допуска случайното прилагане на операциите на IntArray върху обекти на класа IntStack. Освен това, ако един обект от класа IntStack случайно бъде присвоен на обект от класа IntArray целостта на стека може сериозно да бъде повредена. Предотвратяването на такива инциденти е втората причина за дефинирането на IntArray като базов клас private на IntStack. Един базов клас private не може да бъде присвояван на класов обект от произлезлия му клас. Ето един пример за обекти от клас IntStack:

#include <stream.h>
#include "IntStack.h"

IntStack myStaack;
const DEPTH = 7;
main() {
for ( int i = 0; i < DEPTH; ++i ) myStack.push(i);
for ( i = 0; i < DEPTH; ++i ) cout << myStack.pop() << "t";
cout << "n"; }

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

6 5 4 3 2 1 0

Упражнение 3-3. Как мислите, защо С++ не е наречен ++С?

3.6. Операторът sizeof

Операторът sizeof връща размера в байтове на израз или типов спецификатор. Той може да бъде използуван в една от следните две форми:

sizeof (type-specifier);
sizeof expr;

Ето пример за използуването на двете форми:

int ia[] = { 0, 1, 2 };
const sz = sizeof ( ia ) / sizeof ( int );

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

#include <stream.h>
#include "IntStack.h"
main() {
cout << "int :tt" << sizeof( int );
cout << "nint* :tt" << sizeof( int* );
cout << "nint& :tt" << sizeof( int& );
cout << "nint[3] :t" << sizeof( int[3] );
cout << "nn"; // ot separate output
cout << "Intstack :t" << sizeof( IntStack );
cout << "nIntstack* :t" << sizeof( IntStack* );
cout << "nIntstack& :t" << sizeof( IntStack& );
cout << "nIntstack[3] :t" << sizeof(IntStack[3]);

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

int : 4
int* : 4
int& : 4
int[3] : 12

IntStack : 12
IntStack* : 4
IntStack& : 4
IntStack[3] : 36



3.7. Аритметичен оператор if

Аритметичният оператор if е един триместен (тернарен) оператор, който има следния синтаксис:

expr1 ? expr2 : expr3;

expr1 винаги се изчислява. Ако стойността му е истина - т.е. някаква ненулева стойност - се изчислява expr2; иначе - expr3. Следната програма илюстрира как може да бъде използуван аритметичния оператор if.

#include <stream.h>
main() {
int i = 10, j = 20, k = 30;
cout << "nThe Larger value of " << i << " and " << j << " is " << ( i > j ? i : j );
cout << "nThe value of " << i << " is" << ( i % 2 ? " " : " not " ) << "odd";
// the arithmetic if can be nested, bit
// too deep a nesting will be difficult to read
// max is set to the largest of 3 variables

int max = ( ( i>j )
? ( ( i>k ) ? i : k )
: ( ( j>k ) ? j : k ) );
cout << "nThe larger value of " << i << ", " << j << " and " << k << " is " << max << "n"; }

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

The larger value of 10 and 20 is 20
The value of 10 is not odd
The larger value of 10, 20 and 30 is 30


3.8. Оператори за работа с битове

Тези оператори разглеждат операндите си като подредена съвкупност от битове. Всеки бит може да има стойност 0 (off) или 1 (on). Операторите за работа с битове позволяват на програмиста да проверява и инициализира индивидуални битове или битови подмножества.

Операндите на тези оператори трябва да бъдат от тип integer. Въпреки че те могат да бъдат със или без знак препоръчва се използването на операнди без знак. Как точно се обработва знаковия бит при тези оператори зависи от реализацията им; програми, които работят с дадена реализация може да не работят с друга. По такъв начин използуването на операнд без знак подпомага осигуряването на мобилност на програмата.

Първо ние ще обясним как работи всеки оператор. После ще дадем примери за използуване на всеки от операторите за работа с битове. В раздел 6.4 ще реализираме класа BitVector.

Оператори за работа с битове

~ поразредно логическо допълване до 1
~expr1

<< изместване в ляво
expr1 << expr2

>> изместване в дясно
expr1 >> expr2

& поразредно логическо И
expr1 & expr2

| поразредно логическо ИЛИ
expr1 | expr2


Операторът ("~") обръща битовете на операнда си. Всеки бит със стойност 1 става 0 и всеки нулев бит получава стойност 1.

unsigned char bits = 0227;
1 0 0 1 0 1 1 1

bits = ~bits;
0 1 1 0 1 0 0 0

Операторите ("<< , >>") изместват битовете на левия операнд с някакъв брой позиции в ляво или в дясно.

unsigned char bits = 1;
0 0 0 0 0 0 0 1

bits = bits << 1;
0 0 0 0 0 0 1 0

bits = bits << 2;
0 0 0 0 1 0 0 0

bits = bits >> 3;
0 0 0 0 0 0 0 1

Излишните битове на операндите се отстраняват. Операторът за изместване в ляво ("<<") вмъква битове със стойност 0 от дясно. Операторът за изместване в дясно (">>") вмъква битове 76. със стойност 0 от ляво. Ако операндът има знак то може да се добавят копия на знаковия бит или нулеви битове; това зависи от машината.

Операторът AND ("&") работи с два операнда. Резултатът за всяка битова позиция е 1 ако двата операнда съдържат битове със стойност 1; иначе резултатът е нулев бит. (Този оператор не трябва да бъде бъркан с логическия оператор AND ("&&").

unsigned char result:

unsigned char b1 = 0145;
0 1 1 0 0 1 0 1

unsigned char b2 = 0257;
1 0 1 0 1 1 1 1

result = b1 & b2;
0 0 1 0 0 1 0 1

Операторът XOR (изключващо или) ("^") работи с два операнда. Резултатът за всяка битова позиция е 1 ако един от двата, но не и двата операнда съдържат битове със стойност 1; иначе резултатът е нулев бит.

result = b1 ^ b2;
1 1 0 0 1 0 1 0

Операторът OR ("|") работи с два операнда. Резултатът за всяка битова позиция е 1 ако един от двата или двата операнда съдържат битове със стойност 1; иначе резултатът е нулев бит.

result = b1 | b2;
1 1 1 0 1 1 1 1

Променлива, използувана като дискретна съвкупност от битове се нарича битов вектор. Битовият вектор е едно ефективно средство за съхраняване на да/не информация за набор от елементи и условия.

Ето един пример. Нека учител има 30 студента в даден клас.

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

unsigned int quiz1 = 0; Учителят трябва да може да записва във всеки бит 0 или 1, както и да проверява съдържанието му. Например, студентът с номер 27 си е взел изпита и преминава. Учителят трябва да запише 1 в съответния бит. Първата стъпка е да запише в 27-я бит 1 като останалите битове запазят стойността си. Това може да бъде направено чрез оператора за изместване в ляво ("<<") и цялата константа 1.

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1
0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 << 27
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

Ако тази стойност се използува като операнд за OR заедно с quiz1 всички битове освен 27-я ще останат непроменени. А 27-я ще получи стойност 1:

quiz1 |= 1<<27;

Представете си, че учителят повтори изпитването и установи, че студент 27 не се справя задоволително. Той трябва да може да запише отново 0 в бит 27. Този път цялото число трябва да има във всички битове стойност 1 с изключение на 27-я. Забележете, че това число е инверсно на числото от предния пример. Така, че може да бъде използуван оператора NOT:

1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 ~(1 << 27)
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

Ако тази стойност се използува като операнд за AND заедно с quiz1 всички битове освен 27-я ще останат непроменени. А 27-я ще получи стойност 0:

quiz1 &= ~(1 << 27);

Ето как учителят би могъл да провери стойността на различните битове. Да разгледаме пак студент 27. Първата стъпка е да се запише в 27-я бит на едно цяло число 1. Като се приложи оператора AND над това число и quiz1 ще получим истина ако бит 27 на quiz е също 1; иначе ще бъде върната 0 (лъжа).

int hasPassed = quiz1 & (1 << 27);

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

unsigned int ui1 = 3;

i2 = 7; Какъв е резултата на изразите:

(a) ui1 & ui2 (c) ui1 | ui2
(b) ui1 && ui2 (d) ui1 || ui2

Упражнение 3-5. Какво означава присвояването на 0377 на променлива от тип unsigned char в термините на битовия й шаблон? Начертайте картинката.

Упражнение 3-6. Как програмистът може да изолира втория байт на променлива от тип int използувайки операторите за работа с битове?

Упражнение 3-8. Съществува метод за получаване на степените на двойката като се използува оператора за изместване в ляво и константата 1. Генерирайте таблица от първите 16 стойности.

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

6 + 3 * 4 / 2 + 2 Едно изчисление от ляво на дясно дава резултат 20. Други възможни резултати са 9, 14 и 36. Кой е правилния? 14.

В С++ умножението и делението имат по-висок приоритет от събирането. Това означава, че те се изпълняват първи. Обаче, умножението и делението имат еднакъв приоритет. Операции, които имат еднакъв приоритет се изпълняват от дясно на ляво. Следователно редът за изчисляване на израза е:

1. 3 * 4 => 12 2. 12 / 2 => 6 3. 6 + 6 => 12 4. 12 + 2 => 14

Ето един смесен израз, в който има коварна грешка. Проблемът се състои в това, че операторът за неравенство ("!=") има по-висок приоритет от оператора за присвояване:

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

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

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

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

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

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

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

Упражнение 3-9. Трите израза по-горе се изчисляват по начин, противоречащ на намеренията на програмиста. Поставете скоби така, като считате, че програмистът би желал.

Упражнение 3-10. Защо се получава грешка от следния кодов фрагмент? Как бихте могли да я откриете?

void doSomething();
main() {
int i = doSomething(), 0;
}


3.10. Преобразуване на типове

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

Някои от преобразуванията не са безопасни; обикновено компилаторът предупреждава за тях. Преобразуването на по-широкообхватен даннов тип към по-теснообхватен е определено една небезопасна операция. Ето три примера за това:

long lval; unsigned char uc;

int (3.14159); (signed char) uc; short (lval);

Приоритет на операциите и асоциативност

Ниво - Оператор - Функция

17R :: глобален обхват (унарна)
17R :: класов обхват (бинарна)
16R ->,. селектор на член
16R [] индекс на масив
16R () извикване на функция
16R () контруктор на тип
16R sizeof размер в битове
15R ++,-- увеличаване/намаляване с 1
15R ~ поразр.логическо допълване до 1
15R ! логическо не
15R +,- унарни минус,плюс
15R *,& указаван, адрес на
15R () преобразуване на тип
15R new,delete управление на свободна памет
14L ->*,.* селектор за член-указател
13L *,/,% мултипликативни оператори
12L +,- аритметични оператори
11L <<,>> побитово изместване
10L <,<=,>,>= операции за сравнение
9L ==,!= равенство,неравенство
8L & поразредно и
7L ^ поразредно изключващо или
6L | поразредно или
5L && логическо и
4L || логическо или
3L ?: аритметичен оператор if
2R =,*=,/= оператори за присвояване
2R %=,+=,-=,<<=,
2R >>=,&=,|=,^=
1L , оператор запетая


Следните два записа,
type (expr)
(type) expr

Могат да бъдат наречени конвертиращи. Те представят явното изискване на програмиста за преобразуване на expr към тип type. Трите примера илюстрират потенциалната опастност от стесняването при преобразуването на типовете.

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

// 3.14159 != 3.0
3=14159 != double (int (3.141559) );

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

При третия случай за всяка стойност на lval, за която са необходими битове, повече от тези за short, резултатът от преобразуването е недефиниран.

Някои пребразувания са безопасни върху някои машини, но при други предизвикват стесняване. За повечето машини, например, числото int има същия размер както short или long, но не и както двете. Едно от следните преобразувания няма да бъде безопасно върху произволна машина, която не реализира int, short и long в три различни размера.

unsigned short us;
unsigned int ui;
int( us );
long( ui );

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

Неявно преобразуване на типове

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

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

void ff( int );
int val = 3.14159; // converts to int 3
ff( 3.14159 ); // converts to int 3

И в двата случая константата от тип double 3.14159 се преобразува към тип int от компилатора. Може да бъде издадено предупреждение когато конвертирането предизвиква стесняване. При аритметичните изрази преобразуването се насочва към по-широкообхватни типове данни. Например,

val + 3.141559;

По-широкообхватният тип данни в този аритметичен израз е типа double. val неявно се конвертира към типа double чрез разширяване (наричано също повишаване на типа).

Нейната стойност 3 става 3.0 и се добавя към 3.141519. Резултатът от израза е 6.14159.

Забележете, че стойността на val остава 3. Операцията за преобразуване на типа се прилага върху копие на стойността на val. Променливата не се записва в процеса на преобразуване на типа. val = val + 3.14159;

В този израз има две конвертирания. Стойността на val се повишава до тип double. Резултатът 6.14159 се свива до типа int. Получената стойност се присвоява на val. val сега съдържа стойността 6.

Процедурата е съвсем същата когата изразът е записан така:val += 3.14159;

Например, стойността-резултат на следните два израза е 23, а не 20:

int i = 10;
i *= 2.3; // 23,
not 20

Отрязването на дробната част на константата от тип double става след умножението.


Явно преобразуване
Може да се каже, че е малко разточително да се изпълняват две операции за конвертиране в един смесен оператор i = i + 3.14159. Понеже типът на резултата е int изглежда по-разумно да се свива операнда от тип double вместо да се разширява i до тип double и след това сумата да се свива до int.

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

i = i + int(3.14159);

Сега 3.14159 се конвертира към тип int със стойност 3. Тази стойност се добавя към, и после се присвоява на i.

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

Обаче, към данните, сочени от указател void* не можем да се обръщаме директно. ( Няма налична информация за тип, от която да се ръководи компилатора при интерпретирането на сочената битова последователност). По-скоро указателят void* трябва първо да бъде конвертиран към конкретен тип.

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

int i;
void *vp;
int *ip = &i;
double *dp;
vp = ip; // ok:
explicit castdp =vp; // error:
no standart convertion:
unsafe

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

dp = (int*)vp; // ok:
explicit cast*dp = 3=14; // trouble if dp addresses i

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

Даден е следния набор от идентификатори:

char ch;
unsigned char unChar;
short sh;
unsigned short unShort;
int intVal;
unsigned int unInt;
long longVal;
float f1;

Упражнение 3-11. Определете кои от следните присвоявания не са безопасни поради възможното стесняване:

(a) sh = intVal (e) longVal = unInt (b)intVal = longVal
(f) unInt = f1
(c) sh = unChar
(g) intVal = unShort

Упражнение 3-12. Определете типа на следните изрази:

(a) `aґ - 3
(b) intVal * longVal - ch
(c) f1 + longVal / sh
(d) unInt + (unsigned int) longVal
(e) ch + unChar + longVal + unInt

3.11. Оператори

Операторите са най-малките изпълними единици в една С++ програма. Те се разделят посредством точка и запетая; най-простата форма на оператор е празен или нулев оператор. Празният оператор изглежда така:

; // null statement

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

while ( *string++ = *inBuf++ ); // null statement

Присъствието на ненужен празен оператор няма да предизвика грешка по време на компилация. (На автора веднъж се случи да използува компилатор на ALGOL68 в Колумбийския университет, който отбелязваше всеки празен оператор като фатална грешка. Представете си, че започвате от 3 ч. следобед да чакате 40-минутната компилация на програмата си, за да получите странично канцилиране, следствие на точка и запетая).

int val;; // additional null statement

Това е съвкупност от два оператора: декларативен оператор int val и празен оператор.

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

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

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

if ( account.balance - withdrawal < 0 ) {
// compound statement
issueNotice( account.number );
chargePenalty( account.number );
}

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

Съставен оператор, който съдържа един или повече декларативни оператори, се нарича блок. Блоковете се разглеждат подробно в Раздел 3.10 при обсъждането на областите на действие.


3.12. Оператори за управление

Подразбиращият се начин за изпълнение на операторите в една програма е последователен. Изпълнението на всяка С++ програма започва от първия оператор на main(). След това се изпълнява всеки следващ оператор. Когато се изпълни последния оператор изпълнението на програмата приключва.

Като изключим най-простите програми, последователното изпълнение на програмата не съответствува на проблемите, които трябва да бъдат решавани. В примерните програми, които вече разгледахме, се запознахме с условния оператор if и операторите за цикъл while и for. Следващият раздел преглежда целият набор от С++ оператори.

3.13. Операторът if

Всеки оператор if проверява някакво условие. Винаги, когато условието има стойност истина се изпълнява някакво действие или набор от действия. Иначе тези действия се игнорират.

Синтаксисът на оператора if има вида:

if ( expression )
statement;

Изразът expression трябва да бъде затворен в скоби. Ако той има ненулева стойност се приема, че условието има стойност истина и се изпълнява оператора statement.

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

1. Ако текущата стойност е равна на минималната стойност трябва броячът да бъде увеличен с единица.
2. Ако текущата стойност е по-малка от минималната стойност на минималната се присвоява текущата и броячът става 1. Стойностите на масива ще бъдат преглеждани като се използува оператора for. За намирането на най-малкия елемент

45
Глава 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 ще има размер в битове 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 е предполагаема ключова дума за възможно бъдещо разширение на С++ за поддържане на параме-тризирани типове.

Таблица 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. Константни типове

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

Страници: 1 2 [3] 4