Всички данни и функции на IntArray са достъпни за
IntArrayRC така, както и ако IntArrayRC явно ги е дефинирал. Това е значението
на
class IntArrayRC : public IntArray
Двуеточието (“:”) дефинира
IntArrayRC като произхождащ от IntArray. Такъв клас наследява (т.е. разделя)
членовете на класа, от който е произлязъл. За IntArrayRC може да се мисли като
за едно разширение на класа IntArray, което предлага допълнителното свойство за
проверка дали индексът не надхвърля границата на масива. Исканото свойство се
реализира чрез оператор по следния начин:
int&
IntArrayRC::Оperator[]( int index ){
rangeCheck( index );
return ia[ index
]; }
rangeCkeck() проверява всяка индексна стойност. Ако индексът е
невалиден се издава съобщение за грешка, което предизвиква прекъсване на
програмата. Операторът exit(), е този, който прекъсва програмата и се обръща към
операционната система. Аргументите, изпратени към exit() са стойностите, които
се връщат от програмата. Функцията-прототип на exit() се намира в системния
заглавен файл stdlib.h. Ето реализацията на rangeCheck():
#include
<stdlib.h>
#include <stream.h>
enum { ERR_RANGE = 17
};
void IntArrayRC::rangeCheck( int index ) {
if ( index < 0 || index
>= size ) {
cerr << "Index out of bounds for IntArrayRC:
"
<< "ntsize: " << size<< "ntindex: " index <<
"n";
exit ( ERR_RANGE );
} }
Понеже конструкторите не са наследени
от производния клас, IntArrayRC трябва да дефинира собствен конструктор на
екземпляри,които да се съобразява с допустимия размер. Ето неговата
реализация:
// IntArrayRC need only pass its argument
// to its base
class IntArray constructor
IntArrayRC::IntArrayRC( int sz ): IntArray( sz
)()
// null body
Тази част от конструктора, която започва с двоеточие,
се нарича инициализационен списък за член.
Този списък предлага
механизъм, чрез който на конструктора на IntArray се изпраща неговия аргумент.
Тялото на конструктора на IntArrayRC е празно, тъй като задачата му е само да
изпрати аргумента, задаващ размерността, на конструктора на IntArray. Ето един
пример за това как би могъл да бъде използуван един обект от тип
IntArrayRC:
#include "IntArrayRC.h"
const size = 12;
main()
{
IntArrayRC ia( size );
// subscript error: 1..size
for ( int i = 1; i
<= size; ++i )
ia[ i ] = i;
}
Тази програма неправилно индексира
ia от 1 до size вместо от 0 до size-1. Когато се компилира и изпълни, тази
прорама ще даде следния изход:
Index out of bounds for
IntArrayRC:
size: 12 index: 12
Както беше показано от примера,
проверката за принадлежност на индекса на обхвата на масива предлага една добра
защита при използуването на типа масив. Може да се каже, обаче, че тази проверка
ни струва твърде скъпо, защото се извършва само по време на изпълнение на
програмата. Бихме могли да поискаме да комбинираме класовите типове IntArray и
IntArratRC в различни части на програмата си. Класовата наследственост поддържа
това по два различни начина:
1. Към класа, който има наследници, наричан
базов клас, могат да се обръщат и обекти, представители на произлезли обекти.
Например:
#include "IntArray.h"
void swap elements &ia, int i,
int j ) {
// swap elements i and j within ia
int tmp = ia< i ];
ia[
i ] = ia[ j ];
ia[ j ] = tmp;
}
Към swap() могат да бъдат изпращани
аргументи от клас IntArray или аргументи от класове, произлезли от IntArray,
такива като IntArrayRC. Например, дадени са следните два класови
обекта:
IntArray ia1;
IntArrayRC ia2;
за които са валидни
следните две извиквания на swap():
swap( ia1, 4, 7 );
swap( ia2, 4, 7
);
Способността за свързване на произхождащи класови обекти с обекти от
базовия клас позволява взаимозаменяемото използуване на наследствените
класове.
Съществува, обаче, един проблем. Индексният оператор е
реализиран по различен начин в двата класа цели масиви. Когато
извикваме
swap( ia1, 4, 7 );
трябва да бъде използуван индексният
оператор на IntArray. Когато, обаче, извикваме
swap( ia2, 4, 7
);
трябва да бъде използуван индексният оператор на IntArrayRC. За да
бъде използуван този оператор през swap() би следвало да бъде променян при всяко
извикване, в зависимост от конкретния класов тип на аргумента. Този проблем се
решава автоматично от езиковият компилатор на С++, като се използуват механизми,
свързани с виртуални класови функции.
2. Виртуалните класови членски
функции са наследени членове, като например индексния оператор, чиято реализация
зависи от типа на класа.
За да направим индексния оператор виртуален
трябва да променим декларацията му в тялото на класа IntArray:
class
IntArray {
public:
virtual int& operator[]( int
);
...
}
Сега при всяко обръщение към swap() ще бъде извикван
съответния индексен оператор в зависимост от конкретния тип на класа, за който
swap() е извикана. Ето един пример:
#include <stream.h>
#include
"IntArray.h:
#include "IntArrayRC.h"
void swap( IntArray&, int,
int);
main() {
const size = 10;
IntArray ia1( size );
IntArrayRC
ia2( size ); // error: shoud be size-1
cout << "swap() with IntArray
ia1n";
swap( ia1, 1, size );
cout << "swap() with IntArrayRC
ia2n";
swap( ia2, 1, size );
}
Когато се компилира и изпълни, тази
програма дава следния резултат:
swap() with IntArray ia1
swap() with
IntArrayRC ia2
Index out of bounds for IntArrayRC:
size: 10 Index:
10
Упражнение 1-16. Опишете други представители на класа IntArray. Какви
допълнителни операции или данни могат да се добавят? Необходимо ли е да бъдат
заменени някои от операциите на IntArray? Кои?
2.9. Имена на
типове
За масивите и указателите може да се мисли като за производни
типове. Те са конструирани на основата на други типове чрез прилагане на
индексна и съотнасяща операция за дефиниране на специални променливи; за тези
оператори може да се мисли като за конструктори на типове. Всеки производен тип
може по-нататък да служи за базов на други производни типове, така както е при
дефинирането на масив от указатели:
char *winter[ 3 ];
char *spring[]
= { "March", "April", "May" };
winter и spring са масиви. Всеки от тях
съдържа по три елемента от тип char*. spring е инициализиран. Операторът
char
*cruellestMonth = spring[ 1 ];
инициализира cruellestMonth чрез “April”,
символният низ, адресиран от втория елемент на spring.
Следните два
оператора за изход са еквивалентни:
main() {
cout << "Lilacs
breed in " << spring[ 1 ];
cout << "Lilacs breed in "<<
cruellestMonth;
}
Механизмът typedef предлага едно най-общо улеснение
за въвеждане на мнемонични синоними за съществуващи предварително дефинирани,
производни и потребителско дефинирани даннови типове. Например,
class
IntArray;
typedef double wages;
typedef IntArray testScores;
typedef
unsigned int bitVector;
typedef char *string;
typedef string
monthTaable[3];
Тези имена на типове могат да служат като типови
спецификатори вътре в програмата:
const classSize = 93;
string myName
= "stan";
wages hourly, weekly;
testScores finalExam( classSize
);
monthTable summer, fall = { "September", "October", "November"
};
Дефиницията typedef започва с ключовата дума typedef, следвана от
даннов тип и идентификатор. Идентификаторът, или името на типа, не въвежда нов
тип, а по-скоро задава синоним на съществуващ даннов тип. Името на тип,
дефинирано чрез typedef може да се използува навсякъде в текста на програмата,
където могат да се използуват и обичайните имена на типове.
Името на
типа, зададено чрез typedef, може да служи за подпомагане документирането на
програмата. Те също се използуват за да намалят сложността на декларациите.
Обикновено typedef-имената се използуват за подобряване на читаемостта на
дефинициите на указатели към функции и указатели към член-функции на класове.
(Тези типове указатели са разгледани в глави 5 и 6). Име, дефинирано чрез
typedef, може също да бъде използувано за капсулиране на някои машинно зависими
аспекти на програмата. За някои машини, например, типът int може да бъде
достатъчно голям за да побира множество от стойности; за други - може да е
необходим типа long. Тогава ще е необходимо да бъде променен само един опeратор
typedef, когато дадена програма се прехвърля от една машина на друга.