Наследование — механизм создания нового класса из старого. То есть, к существующему классу можно что-либо добавлять, или изменять его каким-то образом для создания нового (порожденного) класса. Это мощный механизм для повторного использования кода. Наследование позволяет создавать иерархию связанных типов, совместно использующих код и интерфейс.
Большинство полезных типов представляют собой различные варианты друг друга, поэтому утомительно выписывать для каждого один и тот же код. Порожденный класс наследует описание основного класса. Затем он может изменяться с помощью добавления членов, изменения функций существующих членов и изменения привилегий доступа. Удобство этого понятия можно показать на примере таксономической классификации, компактно подводящей итог больших областей знания. Например, если известно понятие “млекопитающее”, а так же, что слон и мышь являются млекопитающими, то их описания можно сделать значительно более сжатыми, чем в другом случае. Корневое понятие “млекопитающее” содержит информацию о том, что млекопитающие — это теплокровные животные, относящиеся к высшим позвоночным, которые выкармливают своих детенышей молоком, производящимся в их молочных железах. Эта информация унаследована и мышью, и слоном, но она выражается только один раз: в корневом понятии “млекопитающее”. В C++ это означает, что признаки обоих классов, слона и мыши, порождены от основного класса, “млекопитающее”.
В терминах ООП, оператор “elephant ISA mammal’ описывает отношение. Если есть цирк, в котором есть несколько слонов, то в этом случае объект цирк может иметь члены типа слон. В таком случае, выражение “class circus HASA elephant’ описывает отношение подразделения.
C++ поддерживает virtual функции-члены, которые объявлены в основном классе и переопределены в порожденном классе. Иерархия классов, определенная общим наследованием, создает связанный набор типов пользователя, на которые можно ссылаться с помощью указателя базового класса. При обращении к виртуальной функции через этот указатель в C++ выбирается соответствующее функциональное определение во время выполнения. Объект, на который указывается, должен содержать в себе информацию о типе, поскольку различие между ними может быть сделано динамически. Эта особенность типична для ООП кода. Каждый объект “знает”, как на него должны воздействовать. Эта форма полиморфизма называется чистым полиморфизмом.
Способность к наследованию должна быть встроена в программное обеспечение для того, чтобы максимизировать многократное использование кода и позволить естественное моделирование предметной области. С использованием наследования ключевыми элементами методологии ООП становятся:
• разработка соответствующего набора типов;
• проектирование их возможных связей и применение механизма наследования для совместного использования кода;
• использование виртуальных функций для полиморфической обработки связанных объектов.
ПОРОЖДЕННЫЕ КЛАССЫ
Для порождения нового класса от существующего может использоваться следующая форма записи:
class ИмяКласса:(public|protected|private) ИмяБазовогоКласса
{
объявления членов
} ;
Как обычно, ключевое слово class может быть заменено ключевым словом struct, с той разницей, что по умолчанию члены будут public. Одна из особенностей порожденного класса – видимость унаследованных членов. Для определения доступности членов основного класса порожденному классу используются ключевые слова public, protected и private. Пример порождения класса:
enum year ( fresh, sops, junior, senior, grad};
class student { protected:
int student id;
double gpa;
year у ;
char name[30];
public:
student(char* nm, int id, double g, year x);
void print();
};
enum support { ta, ra, fellowship, others-class grad_student: public student { protected:
support s;
char dept[10] ;
char thesis [80];
public:
grad student (char* nm, int id, double g, year x, support t, char* d, char* th);
void print() ;
};
В этом примере grad_student – порожденный класс, a student -базовый класс. Общее наследование также означает, что полученный класс grad_student – подтип класса student. Следовательно, студент, окончивший институт – студент, но студент не может быть студентом, окончившим институт.
Порожденный класс представляет собой модификацию основного класса, которая наследует общие и защищенные члены базового класса. Таким образом, в примере grad_student члены student – student_id,gpa, name, year и print унаследованы. Часто порожденный класс добавляет новые члены к уже существующим членам класса, как в случае grad student, который имеет три новых члена-данных и переопределенную функцию-член print. Функция-член print перекрывается. Это означает то, что порожденный класс имеет реализацию функции-члена, отличающуюся от базового.
//Проверка правил преобразования указателей.
main (){
student s("Mae Pohl", 100, 3.425, fresh), *ps = &s ;
grad student gs("Morris Pohl", 200, 3.2564, grad, ta,
"Pharmacy", "Retail Pharmacies"), *pgs;
ps -> print(); //student::print
ps = pgs = &gs;
ps -> print(); //student::print
pgs -> print(); //grad_student::print }
Видимость в производном классе
Ограничение доступа private, protected или public для базового класса имеют следующий смысл для зоны видимости членов базового класса в производном. По умолчанию устанавливается private.
• Скрытые члены базового класса недоступны в производном.
• При объявлении Private – открытые и защищенные члены базового класса в производном классе становятся закрытыми.
• При объявлении Protected – открытые и защищенные члены базового класса в производном классе становятся защищенными .
• При объявлении Public – открытые и защищенные члены базового класса в производном классе становятся открытыми и защищенными соответственно.
Графически эти правила представлены на рисунке :
Объявление Базовый класс Производный класс
Private
private private
protected protected
public public
Protected private private
protected protected
public public
Public
private private
protected protected
public public
ПОВТОРНОЕ ИСПОЛЬЗОВАНИЕ КОДА:
ГРАНИЦЫ ДИНАМИЧЕСКОГО МАССИВА
Приведем пример класса безопасного массива vect. Используем код повторно и расширим этот тип до безопасного массива с динамическими пределами – верхним и нижним. Такой стиль объявления массива более гибок и позволяет индексам непосредственно соответствовать прикладной области. Вспомните, что тип динамического безопасного массива vect проверяет индексы на нахождение в диапазоне пределов массива и создает массивы с использованием свободной памяти. Объявление класса следующее:
//Реализация типа динамического безопасного массива vect
class vect
{private:
int *p; //базовый указатель int
size; //число элементов
public: //конструкторы и деструкторы
vect(); //создает массив размерностью 10
vect(int 1); //создает массив размерностью 1
vect(vect& v); //инициализация от vect
vect(int a[], int 1); //инициализация от массива
~vect() { delete [] р; }
int ub() { return (size - 1); } //верхняя граница ints
operator[](int i) ; //получение элемента
//проверенного на соответствие границам
vect& operators(vect& v);
vect operator+(vect& v);
};
Производный тип будет иметь private члены 1_bnd и u_bnd, которые будут хранить нижний и верхний пределы созданного безопасного массива. Производный тип повторно использует представление и код исходного типа.
//Порожденный сип безопасного массива — класс vect_bnd class vect_bnd: public vect
{private:
int l_bnd, u_bnd;
public:
vect_bnd() ;
vect bnd(int, int);
int& operator[](int) ;
int ub() { return (u_bnd); }
int lb() { return (l_bnd); }
};
Конструкторы порожденного класса вызывают конструкторы базового класса. При этом используется такая же синтаксическая конструкция, как и для инициализации членов.
заголовок функции: имя_базового_класса(список_параметров)
В более ранних компиляторах C++ в случае одиночного наследования имя базового класса могло быть опущено и понималось неявно. Сейчас это считается анахронизмом.
vect bnd::vect bnd() :vect(10)
{l_bnd = 0;
u_bnd = 9;
}
vect bnd::vect bnd(int lb, int ub) :vect(ub - lb + 1)
{l_bnd = lb;
u_bnd = ub;
}
Обратите внимание на то, как конструкторы порожденного класса вызывают конструкторы базового класса. Дополнительный код инициализирует пару
границ диапазона. В качестве альтернативы это может быть выполнено инициализаций в списке.
vect_bnd::vect_bnd(int lb, int ub):
vect(ub – lb + 1), l_bnd(lb), u_bnd(ub) {}
Можно также повторно использовать код при перегрузке оператора индексации [].
Int& vect_bnd::operator[](int i)
{if (i < l_bnd || u_bnd < i)
{ cerr « “index out of range\n”;
exit(1) ;
};
return (vect::operator[](i – l_bnd));
}
Это будет очень неэффективно. Почему? Потому, что проверка пределов теперь выполняется дважды. Чтобы избежать этого, необходимо внести два изменения. Во-первых, нужно изменить привилегию доступа члена vect: : р на protected, так что бы порожденный класс имел непосредственный доступ к ранее private реализации vect. Это позволяет сделать второе изменение:
использовать р в функции-члене vect_bnd: : operator[](). Теперь код будет более эффективным:
int& vect_bnd::operator [](int i)
{if (i < l_bnd || u_bnd < i)
{cerr « "index out of range\n";
exit(1) ;
};
return (p[i - l_bnd]) ;
}
Обратите внимание на компромисс между повторным использованием кода и эффективностью. Это распространенный случай. Отметьте также, что наследование требует учета трех уровней доступа. Что должно быть строго частным (private), а что будет защищено (protected), зависит от того, что потенциально может быть повторно использовано.
Совместимость типов предка и потомка.
• Класс потомок всегда можно преобразовать в класс предок.
• Всегда и во всех конструкциях класс потомок может использоваться через ссылку на класс предок.
• Ссылка на класс предок может указывать любой из классов потомков без явного преобразования.
• Указатель на класс потомок может содержать класс предок, но требуется явное преобразование типов. Это очень опасная практика.
Class A{ …};
Class B:public A{…};
A A1;*A2;
B B1,*B2;
A=B; //Допустимо
B=A; //Не допустимо
B2=B(&A); //Допустимо но может привести к ошибке
A2=B; //Допустимо
Виртуальные методы. Полиморфизм.
Виртуальные методы объявляются с использованием служебного слова virtual. Методы объявленные один раз виртуальными, будут виртуальными во всех потомках. В потомках они могут объявляются как с использованием слова virtual так и без него, в любом случае они остаются виртуальными.
• Позволяют выбирать функции члены с одним и тем же именем через указатель функции в зависимости от типа созданного объекта, а не от типа указателя.
• Является одним из типов полиморфизма.
• Типы аргументов, их количество, а также тип возвращаемого значения должны быть у перегруженных функций одинаковыми.
• Не могут быть статическими.
Рассмотрим пример использования виртуальных классов.
#include
#include
#include
#include
enum Boolean{False,True};
const
int DefColor=1, BackColor=0;
class TShape
{protected:
int x,y;
int color;
Boolean Visible;
virtual void Paint(int Color)=0;
public:
TShape(int X,int Y,int Color):x(X),y(Y),color(Color){};
void Draw(){Paint(color); Visible=True; };
void Clear(){Paint(BackColor);Visible=False; }
void Move(int X,int Y);
};
void TShape::Move(int X,int Y)
{
if (Visible) Clear();
x=X;
y=Y;
Draw();
}
class TPixel: public TShape
{protected:
void Paint(int Color);
public:
TPixel(int X,int Y,int Color):TShape(X,Y,Color){};
};
void TPixel::Paint(int Color)
{
putpixel(x,y,Color);
}
class TCircle:public TShape
{protected:
int radius;
void Paint(int Color);
public:
TCircle(int X,int Y,int Radius,int Color)
:TShape(X,Y,Color), radius(Radius){};
};
void TCircle::Paint(int Color)
{
setcolor(Color);
circle(x,y,radius);
}
void main()
{TShape Shape(10,10,3); // ОШИБКА нельзя создать экземпляр
// виртуального класса
TShape *PShape;
TPixel Pixel(10,20,15);
TCircle Circle(150,150,50,3);
int gdriver = DETECT, gmode;
initgraph(&gdriver, &gmode, "..\\bgi\\");
if (graphresult() != grOk) /* an error occurred */
{
cout<<"Graphics error"<
cout<<"Press any key to halt"; getch(); exit(1); /* return with error code */ } Pixel.Draw(); Circle.Draw(); PShape=&Pixel; PShape->Clear();
Circle.Move(400,150);
getch();
closegraph();
}
Виртуальные методы могут быть так же и перегружаемыми, но этот подход может привести к неожиданным последствиям.
#include
class A
{public:
int f;
virtual void fun(int a){cout<<"A::fun(int)"<<
virtual void fun(double a){cout<<"A::fun(double)"<<
};
class B:public A
{public:
int c;
virtual void fun(int a){cout<<"B::fun(int)"<<fun(4.5); //A::fun(double)
pa->fun(4); //B::fun(int)
pb=&b;
pb->fun(4.5); //B::fun(int)
pb->fun(4); //B::fun(int)
return 0;
}
Абстрактные классы
Базовый класс иерархии типа обычно содержит ряд виртуальных функций, которые обеспечивают динамическую типизацию. Часто в базовом классе эти виртуальные функции фиктивны и имеют пустое тело. Определенное значение им придают в порожденных классах. В C++ для этой цели применяется чистая виртуальная функция. Чистая виртуальная функция — виртуальная функция-член, тело которой обычно не определено. Запись этого объявления внутри класса следующая:
virtual прототип_функции = 0;
Чистая виртуальная функция используется для того, чтобы “отложить” решение о реализации функции. В ООП терминологии это называется отсроченным методом.
Класс, имеющий, по крайней мере, одну чистую виртуальную функцию — абстрактный класс. Для иерархии типа полезно иметь базовым абстрактный класс. Он содержит общие базовые свойства порожденных классов, но сам по себе не может использоваться для объявления объектов. Напротив, он используется для объявления указателей, которые могут обращаться к объектам подтипа, порожденным от абстрактного класса.
Множественные базовые классы…
• Позволяют порождать класс от более чем одного класса . Порядок представления базовых классов в списке не существенен .
• К членам базовых классов, имеющим одинаковые имена, доступ должен осуществляться через имена базовых классов, которым они принадлежат, при помощи операции разрешения доступа.
class А { public: void f(); /* ... */ };
class В { public: void f(); /* ... */ };
class С : public A, public В { /* ... */ };
С с;
c.f(); // Ошибка: какое f(), из А или из В?
c.A::f(): // Правильно
Наиболее удобно разрешать такую неоднозначность перекрытием в производном классе обеих функций.
class С : public A, public В
{public:
void f() {A::f(); B::f();} // Перекрываем обе функции II...
}
c.f(): // Теперь правильно
• Для классов, порожденных от производных классов с общей базой, по умолчанию существует два экземпляра объекта общей базы.
Struct X{intl; /*...*/X(int);};
class A public X { /* ... */ A( int); };
class В public X { /* ... */ B( int): };
class С public A, public В {/*...*/ C( int);};
Графически это может быть представлено следующим образом:
К членам общего базового класса можно обратиться через имя одного из производных классов, используя оператор разрешения доступа.
С с( 0 );
++c.i; // Ошибка: какое I, из А или из В? ++c.A::i; // Правильно
С * ср = new C( 0 );
Х * хр = ср; // Ошибка: какое X?
Х*хр=(А*)ср; //Правильно
Виртуальные базовые классы…
• Для классов, порожденных от производных классов с общим виртуальным базовым классом, существует только один экземпляр объекта общего базового класса.
• Объявляются включением ключевого слова virtual в спецификатор ограничения доступа базового класса .
struct V {int i; /*...*/V(int);};
class A virtual public V { /* ... */ A( int); };
class В virtual public V { /* ... */ B( int); };
class С public A, public В { /* ... */ C( Int); };
Похожие записи
No user прокомментировали сообщение
Оставить комментарий