Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

metoda_2013

.pdf
Скачиваний:
54
Добавлен:
03.05.2015
Размер:
6.36 Mб
Скачать

ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ

void home() { _cursor = 0; }

char get() { return _screen[_cursor]; } // ...

};

Друзья

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

class Screen {

friend istream& operator>>( istream&, Screen& );

friend ostream& operator<<( ostream&, const Screen& ); public:

// ... оставшаяся часть класса Screen

};

Операторы ввода и вывода теперь могут напрямую обращаться к закрытым членам класса Screen.

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

Область видимости класса

Тело класса определяет область видимости. Объявления членов класса внутри тела вводят их имена в область видимости класса.

Для обращения к ним применяются операторы доступа (точка и стрелка) и оператор разрешения области видимости (::). Когда употребляется оператор доступа, то предшествующее ему имя обозначает объект или указатель на объект типа класса, а следующее за ним имя должно находиться в области видимости этого класса. Аналогично при использовании оператора разрешения области видимости поиск имени, следующего за ним, идет в области видимости класса, имя которого стоит перед оператором.

Однако применение операторов доступа или оператора

330

ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ

разрешения области видимости нужно не всегда. Некоторые части программы сами по себе находятся в области видимости класса, и в них к членам класса можно обращаться напрямую. Одной из таких частей является само определение класса. Имя его члена можно использовать в теле после объявления:

class String { public:

typedef int index_type;

//тип параметра - это на самом деле String::index_type char& operator[]( index_type )

};

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

Инициализация класса

Рассмотрим следующее определение класса:

class Data { public:

int ival; char *ptr;

};

Мнемонические имена класса и обоих его членов сделали бы, конечно, его назначение более понятным для читателя программы, но не дали бы никакой дополнительной информации компилятору. Чтобы компилятор понимал наши намерения, мы должны предоставить одну или несколько перегруженных функций инициализации - конструкторов. Подходящий конструктор выбирается в зависимости от множества начальных значений, указанных при определении объекта. Например, любая из приведенных ниже инструкций представляет корректную инициализацию объекта класса Data:

Data dat01( "Venus and the Graces", 107925 ); Data dat02( "about" );

Data dat03( 107925 ); Data dat04;

Бывают ситуации (как в случае с dat04), когда нам нужен объект

331

ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ

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

Конструктор класса

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

Конструктор по умолчанию Конструктором по умолчанию называется конструктор, который

можно вызывать, не задавая аргументов. Это не значит, что такой конструктор не может принимать аргументов; просто с каждым его формальным параметром ассоциировано значение по умолчанию:

2. Наследование в языке С++.

Наследование — один из четырёх важнейших механизмов объектно-ориентированного программирования (наряду с инкапсуляцией, полиморфизмом и абстракцией), позволяющий описать новый класс на основе уже существующего (родительского), при этом свойства и функциональность родительского класса заимствуются новым классом.

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

Типы наследования

1. Простое наследование Класс, от которого произошло наследование, называется

базовым или родительским (англ. base class). Классы, которые

332

ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ

произошли от базового, называются потомками, наследниками или производными классами (англ. derived class).

В некоторых языках используются абстрактные классы. Абстрактный класс — это класс, содержащий хотя бы один абстрактный метод, он описан в программе, имеет поля, методы и не может использоваться для непосредственного создания объекта. То есть от абстрактного класса можно только наследовать. Объекты создаются только на основе производных классов, наследованных от абстрактного. Например, абстрактным классом может быть базовый класс «сотрудник ВУЗа», от которого наследуются классы «аспирант», «профессор» и т. д. Так как производные классы имеют общие поля и функции (например, поле «год рождения»), то эти члены класса могут быть описаны в базовом классе. В программе создаются объекты на основе классов «аспирант», «профессор», но нет смысла создавать объект на основе класса «сотрудник вуза».

2. Множественное наследование При множественном наследовании у класса может быть более

одного предка. В этом случае класс наследует методы всех предков. Достоинства такого подхода в большей гибкости.

Множественное

наследование

реализовано

в

C++.

Множественное

наследование —

потенциальный

 

источник

ошибок, которые могут возникнуть из-за наличия одинаковых имен методов в предках. В языках, которые позиционируются как наследники C++ (Java, C# и др.), от множественного наследования было решено отказаться в пользу интерфейсов. Практически всегда можно обойтись без использования данного механизма. Однако, если такая необходимость все-таки возникла, то, для разрешения конфликтов использования наследованных методов с одинаковыми именами, возможно, например, применить операцию расширения видимости — «::» — для вызова конкретного метода конкретного родителя.

Большинство современных объектно-ориентированных языков программирования (C#, Java, Delphi и др.) поддерживает возможность одновременно наследоваться от класса-предка и реализовать методы нескольких интерфейсов одним и тем же классом. Этот механизм позволяет во многом заменить множественное наследование — методы интерфейсов необходимо переопределять явно, что исключает ошибки при наследовании функциональности одинаковых методов различных классов-предков.

Наследование в языке C++ class A{ //базовый класс

};

333

ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ

class B : public A{ //public наследование

}

class C : protected A{ //protected наследование

}

class Z : private A{ //private наследование

}

В C++ существует три типа наследования: public, protected, private. Спецификаторы доступа членов базового класса меняются в потомках следующим образом:

при public-наследовании все спецификаторы остаются без изменения.

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

при private-наследовании все спецификаторы меняются на private.

Одним из основных преимуществ public-наследования является то, что указатель на классы—наследники может быть неявно преобразован в указатель на базовый класс, то есть для примера выше можно написать: A* a = new B;

3. Виртуальные функции в языке С++.

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

class employee { char* name;

short department; // ...

employee* next; static employee* list;

334

ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ

public:

employee(char* n, int d); // ...

static void print_list(); virtual void print() const;

};

Служебное слово virtual (виртуальная) показывает, что функция print() может иметь разные версии в разных производных классах, а выбор нужной версии при вызове print() - это задача транслятора. Тип функции указывается в базовом классе и не может быть переопределен в производном классе. Определение виртуальной функции должно даваться для того класса, в котором она была впервые описана. Например:

void employee::print() const

{

cout << name << '\t' << department << '\n'; // ...

}

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

class manager : public employee { employee* group;

short level; // ...

public:

manager(char* n, int d); // ...

void print() const;

};

Место функции print_employee() заняли функции-члены print(), и она стала не нужна. Список служащих строит конструктор employee. Напечатать его можно так:

void employee::print_list()

{

for ( employee* p = list; p; p=p->next) p->print();

}

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

335

ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ

int main()

{

employee e("J.Brown",1234); manager m("J.Smith",2,1234); employee::print_list();

}

напечатает

J.Smith 1234 level 2 J.Brown 1234

Обратите внимание, что функция печати будет работать даже в том случае, если функция employee_list() была написана и оттранслирована еще до того, как был задуман конкретный производный класс manager! Очевидно, что для правильной работы виртуальной функции нужно в каждом объекте класса employee хранить некоторую служебную информацию о типе. Как правило, реализации в качестве такой информации используют просто указатель. Этот указатель хранится только для объектов класса с виртуальными функциями, но не для объектов всех классов, и даже для не для всех объектов производных классов. Дополнительная память отводится только для классов, в которых описаны виртуальные функции. Заметим, что при использовании поля типа, для него все равно нужна дополнительная память.

Если в вызове функции явно указана операция разрешения области видимости ::, например, в вызове manager::print(), то механизм вызова виртуальной функции не действует. Иначе подобный вызов привел бы к бесконечной рекурсии. Уточнение имени функции дает еще один положительный эффект: если виртуальная функция является подстановкой (в этом нет ничего необычного), то в вызове с операцией :: происходит подстановка тела функции. Это эффективный способ вызова, который можно применять в важных случаях, когда одна виртуальная функция обращается к другой с одним и тем же объектом. Пример такого случая - вызов функции manager::print(). Поскольку тип объекта явно задается в самом вызове manager::print(), нет нужды определять его в динамике для функции employee::print(), которая и будет вызываться.

336

ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ

4. Полиморфизм. На примере С++.

Полиморфизм (polymorphism) (от греческого polymorphos) - это свойство, которое позволяет одно и то же имя использовать для решения двух или более схожих, но технически разных задач. Целью полиморфизма, применительно к объектноориентированному программированию, является использование одного имени для задания общих для класса действий. Выполнение каждого конкретного действия будет определяться типом данных. Например для языка Си, в котором полиморфизм поддерживается недостаточно, нахождение абсолютной величины числа требует трёх различных функций: abs(), labs() и fabs(). Эти функции подсчитывают и возвращают абсолютную величину целых, длинных целых и чисел с плавающей точкой соответственно. В С++ каждая из этих функций может быть названа abs(). Тип данных, который используется при вызове функции, определяет, какая конкретная версия функции действительно выполняется. В С++ можно использовать одно имя функции для множества различных действий. Это называется перегрузкой функций (function overloading).

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

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

337

ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ

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

class Car {

public: double Benzin; // бензин void Go( float dist ) {

Benzin -= dist / 10; // 1 литр бензина на 10 км Rasst += dist; // проехали

}

protected: double Rasst; // пройденное расстояние };

class Truck : public Car

{

public: int Kuzov; void Go( float dist ) {

Benzin -= dist / 5; // 1 литр бенза на 5 км Rasst += dist; // проехали

}

};

Стандартны следующие декларации объектов:

Car * car = new Car; Truck * truck = new Truck;

Однако принцип полиморфизма допускает и такую запись:

Car * car = new Truck;

Переменная ссылочного, более общего типа допускает хранение ссылки на дочерний тип.

А вот запись Truck * truck = new Car; будет ошибочной. Однако можно принудительно выполнить подобный оператор, если явно указать приведение типов: Truck * truck = (Truck*)(new Car);

Здесь создается новый экземпляр класса Car, и ссылка на него приводится к типу ссылки на объект класса Truck.

В результате в переменных car и truck будут храниться ссылки на объекты других классов! Переменая car хранит ссылку на объектгрузовик, наследник Car, а переменная truck хранит ссылку на экземпляр родительского класса Car.

Теперь зададим вызовы метода Go(), который имеется и в классе

Car, и в классе Truck:

Car * car = new Truck; car->Go(100);

Truck * truck = (Truck*)(new Car); truck->Go(100);

Статическое связывание - это связывание метода с родительским объектом на этапе компиляции. Так как компилятор не знает, как

338

ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ

конкретно будет работать программа, он всегда исходит из известного и явно заданного типа объекта-владельца метода. Если используется переменная car класса Car, то будет вызван метод Go() класса Car, хотя физически в переменной car хранится экземпляр класса Truck.

Так же и в случае с переменной truck - хотя мы принудительно записали в нее ссылку на объект класса Car, компилятор об этом не знает и исходит из типа переменной truck - вызывает метод

Go() класса Truck.

При этом компилятор можно явно "обмануть" - выполнить приведение типов:

((Car*)truck)->Go(100);

Здесь уже будет вызван метод Go() класса Car, так как владелец метода Go() в левой части выражения явно приведен к типу Car. Динамическое связывание - это связывание метода с объектом, которое происходит во время работы программы. При этом нужный метод определяется уже не типом переменнойвладельца, а реальным типом объекта, ссылку на который она хранит. Но такое связывание возможно только для так называемых виртуальных методов, - разделение типов методов нужно, чтобы подсказать компилятору, где какое связывание реализовывать.

Виртуальный метод - это обычный метод, в заголовке которого используется ключевое слово virtual. Например:

class Car

{

public:

...

virtual void Go( float dist ) { ... }

Теперь метод Go() класса Car описан как виртуальный. Пусть имеются команды

Truck * truck = (Truck*)(new Car); truck->Go(100);

Если бы метод Go() класса Car был невиртуальным, то выполнилось бы статическое связывание. Но в данном случае вызывается метод Go() класса Car, потому что переменная truck типа Truck хранит ссылку на экземпляр класса Car.

5. Инкапсуляция. На примере С++.

Пусть члену класса (неважно функции-члену или члену, представляющему данные) требуется защита от "несанкционированного доступа". Как разумно ограничить

339

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