Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
YaPLR2012_090303.docx
Скачиваний:
16
Добавлен:
22.08.2019
Размер:
439.04 Кб
Скачать
    1. Цели и задачи работы

Ознакомление с механизмом наследования классов и возможностями, которые он предоставляет; получение навыков использования наследования в прикладных программах.

    1. Теоретические положения.

Как известно, наследование и полиморфизм являются одними из основных элементов объектно-ориентированного подхода в программировании. С помощью наследования можно создать общий класс, определяющий общие черты совокупности объектов. Этот класс может наследоваться другими более специфическими классами, каждый из которых может добавить свои элементы в описании объекта. Есть еще одна причина, по которой наследованию придается большая важность: с его помощью поддерживается полиморфизм во время выполнения программы. В С++ полиморфизм поддерживается как на этапе выполнения программы, так и на этапе ее компиляции. В качестве примера полиморфизма на этапе компиляции можно указать перегрузку операторов и функций. Но, к сожалению, при ее использовании нельзя решить всех задач, возникающих в объектно-ориентированном программировании. Поэтому в С++ используется также полиморфизм времени исполнения программы, для чего используются производные классы и виртуальные функции.

Прежде всего, необходимо коснуться терминологии. Класс, который наследуется другим классом, называется базовым классом. Иногда его также называют родительским классом. Класс, выполняющий наследование, называется производным классом или потомком.

Когда один класс наследует другой, то все публичные члены базового класса доступны в производном классе. В противоположность этому частные члены базового класса не доступны внутри производного класса. В связи с этим может возникнуть вопрос: что если необходимо оставить член частным и вместе с тем позволить использовать его производным классам? Для этих целей и используется ключевое слово protected. Защищенный член подобен частному за исключением механизма наследования. При наследовании защищенного члена производный класс также имеет к нему доступ. Таким образом, указав спецификатор доступа protected, можно позволить использовать член внутри иерархии классов и запретить доступ к нему извне этой иерархии. Общая форма наследования классов имеет следующий вид:

class <имя производного класса>: <доступ> <имя базового класса> {

………

<описание производного класса>;

………

};

Здесь доступ определяет, каким способом наследуется базовый класс. Спецификатор доступа может принимать три значения: private, public и protected. В случае, если спецификатор доступа опущен, то по умолчанию подразумевается на его месте спецификатор public. При этом все публичные и защищенные члены базового класса становятся соответственно публичными и защищенными членами производного класса. Если спецификатор доступа имеет значение private, то все публичные и защищенные члены базового класса становятся частными членами производного класса. И, наконец, если спецификатор доступа принимает значение protected, то все публичные и защищенные члены базового класса становятся защищенными членами производного класса. Например:

class X {

protected:

int i, j;

public:

void get_ij() {cin>>i>>j;}

void put_ij() {cout<<i<<” “<<j<<endl;}

};

// В классе Y i и j класса Х становятся частными членами

class Y: private X {

protected:

int k;

public:

int get_k() {return k;}

void make_k() {k = i * j;}

};

/* Класс Z имеет доступ к k класса Y, т.к. он защищенный, но не имеет доступа к i и j, т.к. они частные */

class Z : public Y {

public:

void f();

};

void Z::f()

{ k = 5; // корректно

i = j = 10; // не корректно, т.к. нет доступа

}

При использовании производных классов важно представлять себе, каким образом и когда выполняются конструкторы и деструкторы базового и производного классов. Конструкторы вызываются в том же самом порядке, в каком классы следуют друг за другом в иерархии классов. Поскольку базовый класс ничего не знает про свои производные классы, то его инициализация может быть отделена от инициализации производных классов и производится до их создания, так что конструктор базового класса вызывается перед вызовом конструктора производного класса. В противоположность этому деструктор производного класса вызывается перед деструктором базового класса.

Отдельный вопрос, который необходимо рассмотреть – это передача параметров в базовый класс. Когда базовый класс имеет конструктор с аргументами, производные классы должны явным образом обрабатывать эту ситуацию путем передачи базовому классу необходимых аргументов. Для этого используется расширенная форма конструкторов производных классов, в которые передаются аргументы конструкторам базовых классов:

<порожденный конструктор>(<список аргументов>) : <базовый конструктор> (<список аргументов>)

{

………

}

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

class X {

protected:

int a, b;

public:

X(int i, int j) {a = i; b = j;}

};

class Y : public X {

private:

int c;

public:

Z(int l, int m, int n): X(l, m) { c = n;}

}

void main(void)

{

Z ob(0,1,2);

}

Также один класс может наследовать атрибуты двух и более классов одновременно. Для этого используется список базовых классов, в котором каждый из базовых классов отделен друг от друга запятой. Например:

class X {

protected:

int a;

public:

void make_a(int p) { a = p; }

};

class Y {

protected:

int b;

public:

void make_b(int q) { a = q; }

}

class Z: public X, public Y {

public:

int make_ab() { return a*b; }

}

Поскольку класс Z наследует оба класса X и Y, то он имеет доступ к публичным и защищенным данным обоих классов. При множественном наследовании конструкторы базовых классов вызываются слева направо по порядку их следования в описании производного класса. Деструкторы же вызываются в обратном порядке – справа налево.

Имеется одна особенность использования указателей на классы при наследовании. В общем случае указатель одного типа не может указывать на объект другого типа. Из этого правила есть исключение, которое относится только к производным классам. В С++ указатель на базовый класс может указывать на объект производного класса, полученный из этого базового класса. Пусть имеем некий базовый класс B_class и его производный класс D_class. В С++ любой указатель типа B_class* может также указывать на объект типа D_class. Например, если имеются следующие объявления переменных:

B_class *p;

B_class b_ob;

D_class d_ob;

то следующие присвоения абсолютно законны:

p = &b_ob;

p = &d_ob;

Используя указатель p, можно получить доступ ко всем членам d_ob, которые наследованы от b_ob. Однако специфические члены d_ob не могут быть получены с использованием указателя p (по крайней мере до тех пор, пока не будет осуществлено преобразование типов). Это является следствием того, что указатель «знает» только о членах базового типа и не знает ничего о специфических членах производных типов. Если необходимо получить доступ к элементам производного класса с помощью указателя, имеющего тип указателя на базовый класс, необходимо воспользоваться приведением типов. Например: ((D_class*)p)->function();

Хотя указатель, имеющий тип указателя на базовый класс, может использоваться в качестве указателя на производный объект, обратное не имеет место. Это означает, что указатель на производный класс не может использоваться для доступа к объектам базового типа.

Полиморфизм времени исполнения обеспечивается не только за счет использования производных классов, но и применением виртуальных функций. Виртуальная функция – это функция, объявленная с ключевым словом virtual в базовом классе и переопределенная в одном или нескольких производных классах. Виртуальные функции являются особыми функциями, потому что при вызове объекта производного класса с помощью указателя или ссылки на него С++ генерирует код, который определяет во время исполнения программы, какую функцию вызвать, основываясь на типе объекта. Для разных объектов вызываются разные версии одной и той же виртуальной функции. Класс, содержащий одну или более виртуальных функций, называется полиморфным классом. Если функция объявлена как виртуальная, то она остается таковой вне зависимости от количества уровней в иерархии классов, через которые она прошла. Например, рассмотрим классы, содержащие информацию о геометрических фигурах:

class figure {

protected:

double x, y;

public:

void set(double a, double b) { x = a; y = b; }

virtual double area() { cout<<”No area”; return 0; }

}

class triangle: public figure {

public:

double area() { return 0.5*x*y;}

}

class square: public figure {

public:

double area() { return x*y; }

}

class circle: public figure {

protected:

double r;

public:

circle(double a) { r = a;}

double area() { return 3.14*r*r};

}

void main(void)

{ double res;

figure *p; // создание указателя базового типа

triangle t; square s; circle c(10); // объекты производных типов

p = &t; p->set(1, 1);

res = p->area(); // находим площадь треугольника

p = &s; p->set(2, 2);

res = p->area(); // находим площадь четырехугольника

p = &c; p->set(20, 30);

res = p->area(); // находим площадь окружности

}

Ключевым моментом в использовании виртуальных функций для обеспечения полиморфизма времени исполнения служит то, что используется указатель именно на базовый класс. Полиморфизм времени исполнения достигается только при вызове виртуальной функции с использованием указателя или ссылки на базовый класс. Ничего не мешает использовать виртуальные функции, как и любые другие нормальные функции, однако достичь полиморфизма времени исполнения на этом пути не удастся. Также стоит отметить, что виртуальным может быть и деструктор класса, хотя конструктор виртуальным быть не может.

Когда виртуальная функция не переопределена в производном классе, то при вызове ее в объекте производного класса вызывается версия из базового класса. Однако во многих случаях невозможно ввести содержательное определение виртуальной функции в базовом классе. Например, при объявлении класса figure в предыдущем примере реализация функции area() не несет никакого смысла. Могут быть также такие виртуальные функции, которые обязательно должны быть переопределены в производных классах, без чего эти классы не будут иметь никакого значения. В таких случаях необходим метод, гарантирующий, что производные классы действительно определят все необходимые функции. Язык С++ предлагает в качестве решения этой проблемы чисто виртуальные функции. Это такие функции, которые объявлены в базовом классе, но не имеют в нем определения. Т.к. они не имеют определений, т.е. тел в этом базовом классе, то всякий производный класс обязан иметь свою собственную версию реализации. Для объявления чистой виртуальной функции используется следующая общая форма:

virtual <тип возвращаемого значения> <имя функции> (<список параметров>) = 0;

Например, для определения чисто виртуальной функции area() в классе figure необходимо написать следующее: virtual double area() = 0;

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

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]