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

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




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



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

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] = i*2; // initialize ia.grow();

cout << "new size " << ia.getSize() << "\n";

for ( i = 0; i < ia.getSize(); ++i )

cout << ia[i] << " ";

}

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

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& );

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

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

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

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

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

// declares the same function

extern void print( int *ia, int sz );

void print( int *array, int size );

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

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

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

unsigned char uc;

ff( uc ); // ff(int)

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

extern void ff( const char* ); // ff( const char* )

ff( cp );

extern void ff( char* ); // ff( const char* )

ff( cp );

char *cp;

const char *pcc;

ff( pcc ); // error ambiguous

Последното обръщение е двусмислено понеже 0 точно съответствува на тип int. Но тя може да се съпостави също и на представителя на ff() чрез прилагане на стандартно преобразуване. Преобразуването на 0 към тип указател прави съпоставими двата преставителя на ff().

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

enum Bool { FALSE, TRUE } found;

enum Stat { FAIL, PASS };

extern void ff( Bool );

extern void ff( Stat );

extern void ff( int );

ff( PASS ); // ff( Stat )

ff( 0 ); // ff( int )

ff( found ); // ff( Bool )

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

extern vоid ff(int);

extern void ff(void *);

и обръщението

ff( 0xffbc );

съответства на ff(int) точно, понеже 0xffbc е цяла литерална константа, написана в шестнадесетичен вид. Програмистът може да предизвика обръщение към void* представителя на ff(), обаче, като приложи явно преобразуване. Това може да се направи така

ff( (void ) 0xffbc ); // ff(void )

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



Особености на съпоставянето чрез стадартно преобразуване.

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

extern ff( char* );

extern ff( ddouble );

ff( ‘a’ ); // ff( double );

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

1. произволен числов тип ще бъде съпоставен на формален аргумент от който и да е друг числов тип, включително и unsigned;

2. изброимите типове ще бъдат съпоставени на формални аргументи от числов тип;

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

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

Ето няколко примера

extern ff( char* );

extern ff( void* );

extern ff( double );

main()

{

int i;

ff( i ); // matches ff( double );

ff( &i ); // matches ff( void* );

ff( "a" ); // matches ff( char* );

}

Всички стандартни преобразувания се разглеждат като изискващи еднаква работа. Преобразуването от char към unsigned char, например, няма по-висок приоритет спрямо преобразуването char към double. Близост на типовете не се разглежда. Ако е възможно повече от едно съпоставяне при прилагането на стандартните преобразувания обръщението е двусмислено и се отбелязва от компилатора като грешно. Например, нека е дадена следната двойка от презаредими функции

extern ff( unsigned );

extern ff( float );

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

// each call is ambiguous

ff( ‘a’ );

ff( 0 );

ff( 2uL );

ff( 3=14159 );

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



Съпоставяне на аргументи псевдоними (reference)

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

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

extern ff( char& );

extern ff( short );

int i;

ff( i ); // ff(short), standart conversion

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

ff( ‘a’ ); // ff(char&), exact match



Обръщения, съдържащи множество аргументи

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

extern ff( char*, int );

exetrn ff( int, int );// ff( int, int )

ff( 0, ‘a’ );

Извиква се представителя на ff(), който получава два аргумента от тип int, понеже:

1. Съпоставянето на първия аргумент е максимално добро. 0 точно отговаря на формалния аргумент от тип int.

2. При втория аргумент имаме еднаквост. ‘a’ в една и съща степен съответствува на втория аргумент на двете функции.

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

int i, j;

extern min ( long, long );

extern min ( double, double );// error amabiguous, no "best" match

min( i, j );

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

extern foo( int, int );

extern foo( double, double );

// error ambiguous two "best" matches

foo( ‘a’, 3.14F );

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



Аргументи с подразбиращи се инициализатори

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

extern ff( int );

extern ff( long, int = 0 );

main()

{

ff( 2L ); // matches

ff( long, 0 );

ff( 0, 0 ); // matches

ff( long, int );

ff( 0 ); // matches

ff( int );

ff( 3.14 ); // error ambiguous

}

Последното обръщение е двусмислено, понеже могат да бъдат съпоставени и двата представителя чрез прилагане на стандартни преобразувания. Няма зададен приоритет за ff(int) понеже, тя има точно един аргумент.



Презареждане и обхват

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

extern void print( char* );

extern void print( double ); // ovoerloads print

void fooBar( int ival )

{// separate scope hides both instances of print

extern void print( int );

print("Value ");// error print(char*) is not visible in this scope

print( ival ); // ok

print(int);

}

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

Упражнение 5-8. Как трябва да бъде дефинирана функцията error() за да обработва следните обръщения

error( "Array out of bounds ", index, upperBound );

error( "Division by zero" );

error( "Invalid selection", selectVal );





Презареждане на оператора new

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

void *operator new( long size );

където size указва паметта в байтове, необходима на типа. Всеки представител на new, дефиниран от потребителя, трябва да връща void* и да определя първия се аргумент от тип long. Например, един втори презаредим представител на оператора new се предлага от една от стандартните за С++ билиотеки. Нейният прототип изглежда така

void * oprerator new( long size, void *memAddress );

Аргументът size се предлага автоматично от компилатора. Допълнителният аргумент трябва да бъде зададен чрез списък от аргументи, разделени със запетая, поместен между ключовата дума new и спецификатора на тип.

#include <new.h>

char buf[ sizeof(IntArray) ];

main()

{

// default instance of new

IntArray *pa = new IntArray( 10 );

// operator new( long, void* )

IntArray *pbuf = new (buf) IntArray( 10 );

}



5.4. Указатели към функции

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

sort( array, lowBound, highBound );

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

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

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

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



Типът указател към функция

Какво представлява един указател към функция? Какъв ще бъде типа му? Как може да бъде деклариран? Ето декларацията на quickSort()

void quickSort( int*, int, int );

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

void pf( int, int, int );

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

void (*pf) ( int*, int, int );

Този оператор декларира pf като указател към функция, която има три аргумента и тип за връщане void - т.е., указател от типа на quickSort().

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

void bubbleSort( int*, int, int );

void mergeSort( int*, int, int );

void heapSort( int*, int, int );

Обаче, двойката функции min() и max() декларират един друг тип

int min( int*, int sz );

int max( int*, int sz );

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

int (*pfi) ( int*, int );

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



Инициализация и присвояване

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

получава стойност като неименуван указател от тип

void (*)( int*, int, int );

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

void (*pfv) ( int*, int, int ) = quickSort;

void (*pfv2)( int*, int, int ) = pfv;

Присвояването на стойност се извършва по подобен начин

pfv = quickSort;

pfv2 = pfv;

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

extern int min( int*, int );

extern void (*pfv) ( int*, int, int ) = 0;

extern (*pfi)( int*, int ) = 0;

main()

{

pfi = min; // ok

pfv = min; // error

pfv = pfi; // error

}

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

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

extern void ff( char );

extern void ff( unsigned );

void (*pf)(char) = ff; // ok

void ff(char)

void (*pf2)

(int) = ff; // error no exact match



Извикване

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

#include <stream.h>

extern min( int*, int );

int (*pf)

( int, int ) = min;

const int iaSize = 5;

int ia[ iaSize ] = { 7, 4, 9, 2, 5 };

main()

{ cout << "Direct call min "<< min( ia, iaSize ) << "\n";

cout << "Indirect call min "<< pf( ia, iaSize ) << "\n";

}

min( int* ia, int sz)

{

int minVal = ia[ 0 ];

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

if ( minVal > ia[ i ] )

minVal = ia[ i ];

return minVal;

}

Обръщението pf( ia, iaSize );

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

(*pf)( ia, iaSize );

Двете форми са еквивалентни.

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

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

int (*testCases[10])();

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

extern const SIZE = 10;

extern int (*testCase[SIZE])();

extern int testResults[SIZE];

void runtest()

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

testResults[ i ] = testCases[ i ]();

}

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

extern void quickSort( int*, int, int );

extern void mergeSort( int*, int, int );

extern void heapSsort( int*, int, int );

extern void bubbleSort( int*, int, int );

void ( sortFuncs[] )( int, int, int ) = {

quickSort,

mergeSort,

heapSort,

bubbleSort };

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

void ( **pfSort ) ( int*, int, int ) = sortFuncs;

Двата оператора ** декларират pfSort като указател към указател.

**pSort; има стойност адреса на sortFuncs.

**pfSort; има стойност адреса на quickSort(), първият елемент на sortFuncs - еквивалентно на записа *pfSort[ 0 ];

За да изпълни quickSort() чрез pfSort, програмистът трябва да напише едно от следните две неща

// equivalent invocation

pfSort[ 0 ]( ia, 0, iaSize-1);

// shorthand

(*pfSort[ 0 ])( ia, 0, iaSize-1 );

// explicit

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

exetrn void quickSort( int*, int, int );

void sort( int*, int, int,void (*)(int*, int, int) = quickSort );

Една проста дефиниция на sort() може да изглежда така

void sort( int *ia, int low, int high,void (*pf)(int*, int, int ))

{

if ( !ia ) return;

if ( !pf ) return;

if ( high < low + 2 ) return;

pf( ia, low, high );

}

sort() може да бъде извикана във всики от следните начини

// normally, these would be in a header file

extern int *ia;

extern const iaSize;

extern void quickSort( int*, int, int );

extern void bubbleSort( int*, int, int );

typedef void (*PFV)( int*, int, int );

extern void sort( int*, int, int, PFV=quickSort );

extern void setSortPointer ( PFV& );

PFV mySort;

void ff(){

sort( ia, 0, iaaSize );

sort( ia, 0, iaSsize, bubbleSort );

setSortPointer( mySort );

sort( ia, 0, iaSize, mySort );

}

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

int ( ff( int ))( int, int );

декларира ff() като функция, която получава един аргумент от тип int. Тя връща указател към функция от тип int (*)( int*, int );

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

typedef int (*PFI)( int*, int );

PFI ff( int );

е една еквивалентна декларация на ff().

Упражнение 5-9. В раздел 4.9 беше дефинирана функцията fibonacci(). Дефинирайте указател към функция, който да може да сочи към fibonacci(). Извикайте функцията чрез този указател за да генерирате редица на Фибуначи от осем елемента.

Упражнение 5-10. В раздел 4.10 беше дефинирана функцията binSearch(). Дефинирайте функция search(), която да може да бъде извиквана така

extern int size, val, ia[>int index = search( ia, size, val, binSearch );

_new_handler

_new_handler представлява указател към функция, предлагана от една от стандартните библиотеки на С++, които се разпространяват заедно с езиковата система на AT&T и има стойност 0 по подразбиране. Декларацията на _new_handler изглежда така

void ( *_new_handler ) ();

_new_handler е указател към функция, която има тип за връщане void и не получава аргументи.

Когато изпълнението на функцията new не приключи успешно, се проверява дали _new_pointer не сочи към функция. Ако _new_handler съдържа стойността по подразбиране, new връща 0; иначе се извиква функцията, към която сочи _new_handler. Функцията, към която сочи _new_pointer трябва да бъдедефинирана от потребителя. Освен това, той трябва явно да даде на _new_handler стойност - указател към функция. Това може да бъде направено директно по следния начин

void freeStoreExeption()// to be invoked if new failsextern

StoreExeption_new_handler = freeStoreExeption;// set_new_handler to free

или чрез библиотечната функция set_new_handler(). Последното може да бъде извършено по следния начин

#include <new.h>// new.h contains a declaration of set_new_handler

set_new_handler( freeStoreExeption );// set _new_handler with library function

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

Най-простата функция, с която се свързва _new_handler издава съобщение за грешка и осигурява изход от програмата

#include <stream.h>

#include <stdlib.h>

extern char *progName; // current file

enum Exeptions { FS_EXHAUST = 1, /* ... */ };

void freeStoreExeption()

{

cerr << progName<< " free store exhausted!\n";

// do any clean-up here ...

exit( FS_EXHAUST );

}

Упражнение 5-11. Инициализирайте _new_handler да сочи към freeStoreExeption и изпълнете отново програмата exhaustFreeStore(), дефинирана в раздел 4.1 на тази глава.



5.5. Свързване, безопасно относно типовете

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

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

1. Несъвместими декларации на функция в отделни файлове.

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

Вътрешнофайлови декларации

В първия случай дадена функция непреднамерено е декларирана различно в два отделни файла; двете декларации трябва да представят една и съща функция. Например, във файла token.C функцията addToken() е дефинирана да получава един аргумент от тип unsigned char. Във файла lex.C, където тази функция се вика, addToken() е декларирана да получава един аргумент от тип char.

// in file token.C

addToken( unsigned char tok )

{ /* ... */ } // in file lex.C

extern addToken( char );

Едно обръщение към addToken() в lex.C ще предизвика грешка във фазата на свързващото редактиране. Аргументи от тип unsigned char и char се кодират различно. Функцията addToken(), декларирана в lex.C, ще бъде отбелязана като недефинирана функция. Ако програмата бъде компилирана успешно може да се случи следното:

Компилираната програма се тества на AT&T 3B20. Тя се изпълнява правилно и се изпраща на ново място, където се използват VAX 8550. Компилира се без какъвто и да е проблем. За нещастие, още при първото изпълнение тя приключва неуспешно. Даже най-простият тест на програмата не работи. Какво се случва? Ето част от зададените от Token декларации:

enum Tokens {

// ...

INLINE = 128;

VIRTUAL = 129; // ... };

Обръщението към addToken() изглежда така: curTok = INLINE; addToken( curTok ); Стойностите от тип char са реализирани като тип със знак върху 8550. Върху 3B20 те са реализирани като unsigned. Неправилната декларация на addToken() не е показана на 3B20; на 8550, обаче, всеки знак стойност по-голяма от 127 предизвиква препълване.

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

Погрешните декларации на външни променливи между различни файлове, обаче, не могат да бъдат открити по време на компилация. Грешки от типа на описаната по-долу могат да бъдат открити само по време на изпълнение ако се получи някакво изключение или неправилен изход на програмата. // in token.C unsigned char lastTok = 0; // in lex.C extern char lastTok; // one token history



Няколко думи за заглавните файлове

Описаното използватне на заглавните файлове е фундаментално за предпазване от такъв тип вътрешни грешки. Всеки заглавен файл предлага едно централизирано разполагане на декларациите на всички extern променливи, прототипи на функции, дефиниции на класове и inline функции. Файлове, които трябва да декларират променлива, функция или клас включват заглавен/ни/ файл/ове/.

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

token.h. // in token.h

enum Tokens { /* ... */ };

extern unsigned char lastTok;

extern addToken( unsigned char ); // in lex.C

#include "token.h" // in token.C

#include "token.h"

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

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



Обръщения към функции от други езици

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

extern "C" void exit(int);

extern "C" {

printf( const char* ... );

scanf( const char* ... ); }

extern "C" { #include <string.h> }

Свързващата директива се състои от ключовата дума extern, следвана от низов литерал и един “обичаен” прототип на функция. Въпреки, че функцията е написана на друг език, обръщението към нея също се проверява за типово съответствие. Във фигурните скоби могат да бъдат записани много функции. Те /скобите/ служат като ограничител и не въвеждат ново ниво на обхват.

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

char *copy( char *src, char *dst )

{ // error: linkage directive must be at file scope

extern "C" strlen( const char* );

if ( !dst ) dst = new char [ strlen(scr)+1 ];

//...

return dst; }

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

extern "C" strlen( const char* );

char *copy( char *src, char *dst )

{ if ( !dst ) dst = new char [ strlen(scr)+1 ];

//...

return dst; }

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

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

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

extern "C" strlen( const char* ); // in String.h

extern "C" strlen( const String& ); // in String.h

Презареждането на sqrt(), което следва, илюстрира едно типично използване на свързваща директива:

class Complex;

class BigNum;

extern Complex& sqrt( Complex& );

extern "C" double sqrt( double );

extern BigNum& sqrt(BigNum& );

Представителят sqrt() на С е заобиколен; допълненият представител на С++ класа не е.

Упражнение 5-12. exit(), printf(), malloc(), strcpy() и strlen() са библиотечни функции на езика С. Изменете следната програма на С така, че да може да се компилира и свързва в С++. char *str ="hello";

main() { /* C language program */

char *s, *malloc(), *strcpy();

s = malloc( strlen(str)+1 );

strcpy( s, str );

printf("%s, world\n", s );

exit( 0 ); }

Упражнение 5-13. Раздел 3.10 дефинира функцията binSearch(). Ние желаем да я направим достъпна за програми, написани на С. Как трябва да я декларираме?

Упражнение 5-14. Библиотечните тригонометрични функции на С са декларирани по следния начин: double sin( double ); double cos( double ); double tan( double );

Покажете как тези декларации биха могли да бъдат презаредени за да възприемат аргументи от класовия тип RealNum и Complex?


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



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