ВВЕДЕНИЕ

2.4 Понятие и описание классов и методов объектно-ориентированных языков программирования

2.4.1 Понятие класса и объекта в Си++

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

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

Понятие класса в С++ включает в себя и расширяет структуру и объединение в С. Объявить класс можно с помощью конструкции:

ключ_класса имя_класса { список_компонентов_класса };

где ключ_класса - один из служебных идентификаторов: class, struct или union,

имя_класса - идентификатор класса или типа,

список_компонентов_класса - определения и описания данных и методов, принадлежащих классу.

Список_компонентов_класса называют телом класса.

Пример объявления простого класса может выглядеть следующим образом:

class Simple_Counter {

int count;

public:

void SetCount(int);

int GetCount(); };

Переменная count определена внутри класса и называется переменная-член класса. Функции SetCount(int) и GetCount() являются функциями-членами класса. Эти функции пока не определены, а только объявлены. Их определения могут быть вынесены за тело класса или же могут быть включены в тело класса.

Если определения функций-членов приводятся вне тела класса, то они должны быть оформлены следующим образом:

void Simple_Counter::SetValue ( int val ) { count = val; }

int Simple_Counter::GetValue ( ) { return count; }

При таком определении функции после типа возвращаемого значения надо указывать, какому классу принадлежит данная функция. Для этого пишется имя класса с двумя знаками двоеточия.

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

class Simple_Counter {

int count;

public:

void SetCount(int val) { count = val; }

int GetCount() { return count; } };

Класс считается определенным, если определены все его методы. Для использования класса надо определить объекты этого класса точно также, как и обычные переменные в языке С, например,

Simple_Counter my_count;

my_count называют или переменной, или экземпляром, или объектом класса Simple_Counter. Используя объект класса, можно вызывать функции-члены или обращаться к членам-данным класса.

void main() {

Simple_Counter my_count;

my_count.SetCount(5);

int k = my_count.GetCount();

Simple_Counter * pmy_count = new Simple_Counter;

pmy_count->SetCount(5);

int k = pmy_count->GetCount();

}

Обратите внимание, что обращение к членам класса осуществляется также, как и к элементам структур с использованием знаков -> и . .

При создании класса одной из главных задач программиста является скрытие данных внутри класса. С этой точки зрения существуют три вида пользователей класса:

· сам класс;

· обычные пользователи класса;

· производные классы.

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

· private;

· public;

· protected.

Члены класса с доступом private (приватные) имеют самый ограниченный доступ. Только сами члены-функции класса могут обращаться к приватным членам. Члены класса с доступом public доступны без ограничений. Члены класса с доступом protected доступны в производных классах.

Если не указан явно уровень доступа, то для типа class это доступ private, для типа struct и union - public.

В примере, показанном ниже, на этапе трансляции будут выданы сообщения об ошибке, поскольку private член класса Simple_Counter count недоступен:

void main() {

Simple_Counter my_count;

my_count.count = 5;

int k = my_count.count;

}

Вопросы для самопроверки.

1. Как можно указать тип inline для компонентной функции класса?

2. В чем заключаются отличия между class, struct и union в С++?

3. Можно ли создать класс со всеми членами типа private?

4. Возможно ли создание пустого класса?

class Empty{ };

Как его можно использовать?

5.Что такое вложенный класс? Приведите пример его использования.

6. Как транслируются и выполняются функции-члены класса A TestA_1 и TestA_2?

class A {

public:
const A& TestA_1(const int & );

int & TestA_2(int & ) const;

}

7.Можно ли перегружать функции-члены? Приведите примеры.

8.Могут ли члены-данные иметь атрибут const? Как инициализировать эти члены?

В самом простом виде класс определяется в Си++ как структура, работа с элементами которой возможна только через элементы-функции. В отличие от структуры класс имеет "приватную" (личную) часть, элементы которой не могут быть доступны иначе как через другие элементыфункции, и "публичную" (общую) часть, элементы которой могут быть использованы непосредственно. Объектом называется определяемая в программе переменная, тип которой определен как класс (структура):

Определение структуры Определение класса

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

struct dat { class dat { // Приватная часть

int day,month,year; int day,month,year;

public: // Публичная часть

void SetDat(int,int,int); void SetDat(int,int,int);

void SetDat(char *); void SetDat(char *);

} аа}

void main() { void main() {

// Опред-ние переменных a,b // Опред-ние объектов a,b класса dat

dat a,b; dat a,b;

a.day = 5; //Непосредственное использование

a.month = 12; // приватной части объекта запрещено

bAA.SetDat("12,12,1990"); b.Setdat("12,12,1990");

} }

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

Tаким образом в первом приближении класс отличается от структуры четко определенным интерфейсом доступа к его элементам.

Объекты класса обладают всеми свойствами переменных, в том числе такими, как область действия и класс памяти (время жизни).

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

- статические (внешние), создаваемые в статической памяти программы и существующие в течение всего времени работы программы;

- автоматические, создаваемые в стеке в момент вызова функции и уничтожаемые при ее завершении;

- динамические, создаваемые и уничтожаемые в свободной памяти задачи в моменты вызова функций malloc() и free() или выполнения операторов new и delete.

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

class dat { ....... }

dat a,b; // Статические объекты

dat *p; // Ссылка на объект

void main() {

dat c,d; // Автоматические объекты

p = new dat; // Динамический объект

...

delete p; // Уничтожение динамического объекта

} // Уничтожение автоматических объектов

## 2.4.2 Типы методов

Методы объекта могут быть описаны как статические, виртуальные, динамические или как методы-сообщения. Рассмотрим следующий пример:

TFoo = class

procedure lAmAStatic;

procedure lAmAVirtual; virtual;

procedure lAmADynamic; dynamic;

procedure IAniAMessage(var M: TMessage); message wm SomeMessage;

end;

###

### Статические методы

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

### Виртуальные методы

Метод lAmAVirtual является виртуальным. Вызов таких методов, благодаря возможности их перегрузки, немного сложнее, чем вызов статического метода, так как во время компиляции адрес конкретного вызываемого метода неизвестен. Для решения этой задачи компилятор строит таблицу виртуальных методов (Virtual Method Table. VMT), которая обеспечивает определение адреса метода в процессе выполнения программы. VMT каждого объекта содержит все виртуальные методы предка и виртуальные методы самого объекта, в связи с чем виртуальные методы используют несколько большую память, чем методы динамические, но при этом их вызов происходит быстрее, чем вызов динамических методов.

### Динамические методы

Динамический метод lAmADynamic подобен виртуальным методам, но обслуживаются другой диспетчерской системой. Компилятор назначает уникальное число каждому динамическому методу и использует его вместе с адресом метода для построения таблицы динамических методов (Dynamic Method Table, DMT). В отличие от VMT, DMT содержит только методы данного объекта, тем самым экономя используемую память, но и замедляя вызов метода, так как для поиска его адреса, возможно, придется пересмотреть не одну DMT в иерархии объектов.

### Методы-сообщения

Метолом обработки сообщений служит lAmAMessage. Значение после слова message определяет сообщение, в ответ на которое вызывается данный метод. Такие методы создаются для реакции на те или иные сообщения Windows, их никогда не вызывают непосредственно из программы.

##

## 2.4.3 Замещение методов

Замещение (overriding) метода в Object Pascal реализует концепцию полиморфизма в ООП. Оно позволяет изменять поведение метода от наследника к наследнику. Замещение метода возможно только в том случае, если первоначально он был объявлен как virtual или dynamic. Для замещения метода при его описании вместо ключевых слов virtual или dynamic используется слово override:

TFooChild = class(TFoo)

procedure lAmAVirtual; override;

procedure lAmADynamic; override;

procedure IAmAMessage(var M: TMessage); message wm SomeMessage;

end;

Эта директива замещает оригинал метода в VMT новым методом. Если вы определите новые функции с описателями virtual или dynamic вместо override, то тем самым создадите новый метод вместо замещения старого. Если вы попытаетесь заместить статический метод, он просто полностью заменит статический метод родителя в потомке.

2.4.4 Виртуальные базовые классы

В прямом ациклическом графе наследования класс может появиться более чем один раз. Элементы данных (экземпляры переменных) класса Parent появляются дважды в классе GrandChild. Первый набор наследуется через Child1, второй - через Child2. Такое наследование бывает нежелательно. Виртуальные базовые классы обеспечивают механизм для избежания дублирования элементов в классе, таком как CrandChild

Допустим, класс Parent на самом деле называется Vehicle (транспортное средство). В его протоколе содержится закрытое поле данных int topSpeed. Класс Child1 на самом деле называется Boat(Корабль), а класс Child2 -называется Plane (самолет). Наконец, класс GrandChild на самом деле называется SeaPlane. Для SeaPlane желательно наследовать Boat::topSpeed и Plane::topSpeed. Объект класса SeaPlane имеет различную предельную скорость в зависимости от того, действует он как корабль или самолет.

С другой стороны, допустим, класс Parent назван DomesticAnimal (домашнее животное), класс Child1 назван Cow (корова), класс Child2 назван Buffalo (бык), а класс GrandChild назван Beefalo(теленок). Допустим, протокол класса DomesticAnimal включает экземпляры переменных int weight, float price, char color[20]. Нежелательно, чтобы протокол класса Beefalo имел по два экземпляра переменных weight, price, color. Избежать дублирования позволяет использование виртуальных базовых классов.

class DomesticAnimal {

protected:

int weight; float price; char color[20];

public:

DomesticAnimal(void) {weight=0; price=0.; strcpy(color,”none”); }

DomesticAnimal(int aweight, float aprice,char *acolor) {

weight=aweight; price=aprice; strcpy(color,acolor);

}

virtual void print(void) { cout << weight << price << color; }

};

class Cow: public virtual DomesticAnimal {

public:

Cow(void) { }

Cow(int aweight,float aprice,char *acolor) {

weight=aweight; price=aprice; strcpy(color,acolor); }

void print(void) { cout << “ Cow has propeties”; DomesticAnimal::print(); }

};

class Buffalo: public virtual DomesticAnimal {

public:

Buffalo(void) { }

Buffalo(int aweight,float price,char *acolor) {

weight=aweight; price=aprice; strcpy(color,acolor); }

void print(void) { cout<< “ Buffalo has propeties”;

DomesticAnimal::print(); }

};

class Beefalo: public Cow,public Buffalo {

public:

Beefalo(int aweight,float aprice,char *acolor) {

weight=aweight; price=aprice; strcpy(color,acolor); }

void print(void) { cout << “ beefalo has propeties”;

DomesticAnimal::print(); }

};

main() {

Cow aCow(1400,375.0,”black and white”); // void const Domestic

// const par Cow

Beefalo aBeefalo(1700,525.0,”Brown and black”); // void cons Domestic

// void const Cow

// void const Buffalo

// const par Beefalo

DomesticAnimal& myCow=aCow;

DomesticAnimal& myBeefalo=aBeefalo;

my.Cow.print();

myBeefalo.print();

}

Результат работы

Cow has propeties

weight=1400 price=375.0 color=black and white

beefalo has propeties

weight=1700 price=525.0 color=brown and white

Код приведенной выше программы решает проблему класса Beefalo, который наследует поля данных weight, price, color. Объекты класса Beefalo имеют по одному полю данных для веса, цены и цвета.

· Виртуальные базовые классы инициализируются (вызывается void-конструктор) перед любыми не виртуальными базовыми классами и в том порядке, в котором они появляются в ПАГе наследования при просмотре его снизу-вверх и слева направо.

· Если виртуальный базовый класс имеет хотя бы один конструктор, то он должен иметь void-конструктор.

Ключевое слово virtual в классе Cow и классе Buffalo предотвращает многократное копирование полей данных weight,price,color из предков класса Beefalo.

Другой пример:

class W{

public:

virtual void f(void) { cout << “ W::f”; }

virtual void g(void) {cout << “ W::g”; }

virtual void h(void) {cout << “ W::h”; }

virtual void k(void) {cout << “ W::k”; }

};

class A: public virtual W {

public:

void g(void) { cout << “ A::g”;}

};

class B: public virtual W {

public:

void f(void) { cout << “ B::f”; }

};

class C: public A,public B, public virtual W {

public:

void h(void) { cout << “C::h”; }

void f(void) { B::f(); }

void g(void) { A::g(); }

};

main() {

C *pc=new C;

pc->f(); // C::f() ( B::f)

pc->g(); // C::g() ( A::g)

pc->h(); // C::h

((A *)pc)->f(); // C::f (B::f)

((W *)pc)->f(); // C::f (B::f)

B *pb=new B;

pb->f(); // B::f

pb->g(); // W::g

((W *)pb)->f(); // B::f

A *pa=new A;

pa->f(); // W::f

pa->g(); // A::g

((W *)pa)->g(); // A::g

}

При вызове ((A *)pc)->f() выполняется функция f класса С. Тоже происходит и при вызове ((W *)pc)->f().Таблица виртуальных функций использует определение f в классе С. Это демонстрирует то, что при вызове “верхней” виртуальной функции по одному пути ПАГа может закончиться вызовом функции по другому пути.

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

class V { public: int i; void f(); };

class A: virtual public V {public: int i; int f(); };

class B: virtual public V { };

class C: public A, public B {

void g() { i=f(); } // Правильно A::i и A::f() скрывают V::i V::f()

};

Их конструкторы вызываются конструктором последнего класса в цепочке производных классов.

V::V(int n): i(n) {}

A::A(int i): V(i) {}

B::B(int i): V(i) {}

C::C(int i): V(i), A(i), B(i) {}

В объявлении класса могут быть смешаны с невиртуальными базовыми классами. Виртуальные базовые классы могут вызывать нежелательные множественные вызовы функций-членов в виртуальном базовом классе.

ckass V { public: void f(); };

class A: virtual public V {

public:

void f() { V::f(); }

};

class B: virtual public V {

public:

void f() { V::f(); }

};

class C: public A,public B {

public:

void f() { A::f(); B::f(); } // V::f() вызывается дважды

};

Для преодоления указанного эффекта можно определить функцию real_f(), которая выполняет действия, специфичные для функции f() данного класса. Тогда функция f() будет выполнять последовательные вызовы сначала функции real_f(), а затем - функции f().

class V {

protected:

void real_f() {}

public:

void f() { real_f(); }

};

class A: virtual public V {

protected:

void real_f() {}

public:

void f() { real_f(); V::f(); }

};

class B: virtual public V {

protected:

void real_f() { }

public:

void f() { real_f(); V::f(); }

};

class C: public A, public B {

protected:

void real_f() {}

public:

void f() { real_f(); A::reaal_f(); B::real_f(); V::real_f(); }

};

2.4.5 Порожденные классы

Для порождения нового класса от существующего может использоваться следующая форма записи:

class имя класса : (public | protected | private) имя базового класса {

объявления членов

};

Одна из особенностей порожденного класса - видимость унаследованных членов. Для определения доступности членов основного класса порожденному классу используются ключевые слова public, protected и private. Рассмотрим пример:

class student {

protected:

int id;

char name[30];

public:

sudent(int i,char *nm);

void print();

};

class grad_student : public student {

protected:

char thesis[30];

public:

grad_student(int i,char *nm, char *t);

void print();

};

В этом примере grad_student - порожденный класс, а student - базовый класс. Использование ключевого слова public в заголовке описания порожденного класса означает, что protected и public члены класса student должны быть унаследованы как protected и public члены grad-student соответственно. Члены private недоступны. Общее наследование также означает, что полученный класс grad-student - подтип класса student. Следовательно, студент окончивший институт - студент.

Порожденный класс представляет собой модификацию основного класса, которая наследует общие и защищенные члены базового класса. Таким образом, в примере grad_student члены student - id,name, print унаследованы. Функция-член print перекрывается. Это означает, что порожденный класс имеет реализацию функции-члена, отличающуюся от базового класса. Не следует путать с перегрузкой, где то же самое имя функции может иметь различные значения для каждой уникальной сигнатуры.

Такой механизм выгоден по следующим соображениям:

Код используется многократно. Тип grad_student использует существующий проверенный код из student.

Иерархия отражает отношения, существующие в предметной области. Различные полиморфные механизмы позволяют коду пользователя обрабатывать grad_student как подтип student, что упрощает код пользователя, в тоже время предоставляя ему выгоды при обработке этих различий между подтипами.

Типизированные преобразования и видимость

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

Рассмотрим пример конструктора базового и порожденного класса:

student::student(int i,char *nm):id(i) { name=new char [strlen(nm)+1]; strcpy(name,nm); }

grad_student::grad_student(int i,char *nm,char *t): student(i,nm) {

thesis=new char[strlen(t)+1]; strcpy(thesis,t); }

Конструктор для student вызывается, как часть списка инициализаторов. Логично создавать объект базового класса прежде, чем может быть завершен полный объект.

Ссылка на порожденный класс может быть неявно преобразована в ссылку на общий базовый класс.

grad_student gs(1,”NNN”,”Tn”);

student &rs=&gs;

Переменная rs - ссылка на student. Базовый класс для grad_student - student. Следовательно, такое преобразование ссылки допустимо.

Функция-член print - перегружена.

void student::print() { cout << id << name; }

void grad_student::print() { student::print(); cout << thesis; }

Для того, чтобы grad_student::print вызвала функцию student::print, должен быть использован идентификатор с разрешенной областью student::print. Иначе произойдет бесконечный цикл. Рассмотрим пример отношения между базовым и порожденным классом.

void main(void) {

student s(1,”Ma”), *ps=&s;

gard_student gs(2,”Jh”,”Pet”), *pgs;

ps->print(); // student::print()

ps=pgs=&gs;

ps->print(); // student::print()

pgs->print(); // gard_student::print()

}

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

2.4.6 Абстрактные классы

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

class base {

public:

virtual print() =0;

virtual get() =0;

}

Естественно, что определять тела этих функций не требуется.

2.4.7 Конфликт имен

А что, если один или более элементов двух базовых классов имеют одинаковые имена. Предположим, например, что экземпляры переменных в базовых классах Base1 и Base2 (вариант, в котором вместо private использовали protected) переименованы в instance.

Появится неоднозначность в определении перегруженного оператора <<. Компилятор не сможет отличить d.instnace, ссылающийся на класс Base1, от d.instance, ссылающийся на класс Base2. Если нет обращения на прямую, то все будет работать нормально.

Если данные описаны как private (либо, если для доступа к ним все равно используются функции доступа), то программа будет работать, так как протокол класса Derived может получить доступ к экземплярам переменных двух базовых классов, только используя методы доступа accessid, accessName. Только при попытке обратиться к экземплярам напрямую вы потерпите неудачу из-за конфликта имен.

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

class Base1 {

protected:

int id;

public:

Base1(void) { id=0; }

Base1(int anid) { id=anid; }

};

class Base2 {

protected:

int id;

public:

Base2(void) { id=0; }

Base2(int anid) { id=anid; }

};

class Derived: public Base1,public Base2 {

char ch;

public:

Derived(void) { ch=‘a’; }

Derived(char c,int anid,int aid):Base1(anid),Base2(aid) { ch=c; }

void assign(char c) { ch=c; }

friend ostream& operator<<(osream& o,Derived& d);

};

ostream& operator<<(ostream&o,Derived& d) {

o<<“id Base1”<<d.Base1::id;

o<<“id Base2”<<d.Base2::id;

o<<“ ch”<<c; return o;

}

main() {

Derived object1; cout << object;

Derived object2(‘e’,120,-150); cout << object2;

}

Результаты работы программы

id Base1 0 id Base2 0 ch a

id Base2 120 id Base2 ch e

Объекты класса Derived содержат по два экземпляра переменных каждый. Одна id из класса Base1, а другая - id из класса Base2. Доступ к той или иной переменной в функции-члене может быть осуществлен Base1::id или Base2::id, в дружественной функции через квалификатор d.Base1::id или d.Base2::id.

Рассмотрим еще один пример.

class Base1 {

public:

int cost();

};

class Base2 {

public:

int cost();

};

class Der: public Base1, public Base2 {

public:

int tot_cost() { return (Base1::cost()+Base2::cost()); }

};

void main() {

Der der;

der.cost(); // неоднозначность

}

Компилятор выдаст сообщение об ошибке - неоднозначное обращение. Эта проблема может быть устранена или правильной квалификацией cost с использованием оператора разрешения контекста, или добавлением члена cost к порожденному классу.