Полезное:
Как сделать разговор полезным и приятным
Как сделать объемную звезду своими руками
Как сделать то, что делать не хочется?
Как сделать погремушку
Как сделать так чтобы женщины сами знакомились с вами
Как сделать идею коммерческой
Как сделать хорошую растяжку ног?
Как сделать наш разум здоровым?
Как сделать, чтобы люди обманывали меньше
Вопрос 4. Как сделать так, чтобы вас уважали и ценили?
Как сделать лучше себе и другим людям
Как сделать свидание интересным?
Категории:
АрхитектураАстрономияБиологияГеографияГеологияИнформатикаИскусствоИсторияКулинарияКультураМаркетингМатематикаМедицинаМенеджментОхрана трудаПравоПроизводствоПсихологияРелигияСоциологияСпортТехникаФизикаФилософияХимияЭкологияЭкономикаЭлектроника
|
Динамическое связывание
Ранее говорилось о том, что объектно-ориентированные языки программирования предоставляют средства для поддержки трех концепций: абстракции данных, наследования и динамического связывания. Динамическое связывание - определение на этапе выполнения, какая функция должна быть вызвана для определенного объекта. Словосочетание динамическое связывание означает более точно «динамическое связывание операции c объектом». Чтобы понять эту концепцию, начнем с рассмотрения примера. Пример. Если даны классы Time и ExtTime, описанные ранее, то в результате следующего фрагмента кода создаются два объекта и выводится время, представляемое каждым из них. Time startTime(8, 30, 0); ExtTime endTime(10, 45, 0, CST); startTime.Write(); cout<<”\n”; endTime.Write(); В этом фрагменте вызывается две различных функции Write, даже несмотря на то, что они имеют одно и то же имя. В первом случае вызывается функция Write из класса Time, которая выводит три значения: часы, минуты и секунды. Второй раз вызывается функция Write из класса ExtTime, которая выводит четыре значения: часы, минуты, секунды и часовой пояс. В данном фрагменте кода компилятор использует статическое (на этапе компиляции) связывание операции (Write) с соответствующим объектом. Компилятор может легко определить, какую функцию вызывать, проверив тип данных ассоциированного с ней объекта. Статическое связывание - определение на этапе компиляции, какая функция должна быть вызвана для определенного объекта. В некоторых ситуациях компилятор не может определить тип объекта, поэтому связывание операции с объектом должно производиться в момент выполнения программы. Одна из таких ситуаций - передача объекта класса в качестве параметра. Основное правило языка C++, касающееся передачи объекта класса в качестве параметра, состоит в том, что фактический параметр и соответствующий ему формальный параметр должны иметь одинаковые типы. При использовании наследования, это правило немного смягчается. Вы можете передавать объект производного класса С объекту его родительского класса Р, но не наоборот; то есть, нельзя передать объект типа Р объекту типа С. Обобщая это правило, можно сказать, что разрешается передавать объект потомственного класса объекту любого из его предшествующих классов. У этого правила есть огромное преимущество - оно позволяет написать одну функцию, которая будет применима для всех потомственных классов, вместо того, чтобы писать для каждого из них свою функцию. Пример. Можно написать вымышленную функцию Print, которая получает в качестве параметра объект типа Time или любого класса, потомственного от Time: void Print(Time someTime) {cout<<endl; cout<<"The time is"; someTime.Write(); cout<<endl; } Если есть следующий фрагмент кода Time startTime(8, 30, 0); ExtTime endTime(10,45,0,CST); Print(startTime); Print(endTime); то компилятор позволяет передать в функцию Print либо объект класса Time, либо объект класса ExtTime. Однако, когда выводится содержимое endTime, на экране будет отсутствовать значение часового пояса. Функция Print использует механизм передачи по значению для формального параметра someTime. В результате этого механизма формальному параметру посылается копия фактического параметра. Когда вы посылаете объект производного класса объекту родительского класса, используя передачу по значению, копируются только их общие члены, содержащие данные. Производный класс почти всегда «больше» своего предка - то есть он содержит дополнительные члены с данными. Объект класса Time имеет три члена, содержащих данные (hrs, mins и seсs), а объект класса ExtTime - четыре (hrs, mins, seсs и zone). Когда объект большего класса копируется в меньший с использованием механизма передачи по значению, то лишние члены, содержащие данные, отбрасываются или «разделяются». Такая ситуация называется проблемой разделения. Замечание. Эта же проблема актуальна и для операции присваивания. В результате выражения parentClassObject = childClassObject; будут скопированы значения только общих членов двух объектов, а дополнительные члены объекта childClassObject не будут скопированы. При передаче по ссылке проблемы разделения не существует, так как функции передается адрес фактического параметра. Изменим заголовок функции Print так, чтобы someTime стал параметром, передаваемым по ссылке: void Print(Time& someTime) Теперь, когда мы передаем в качестве фактического параметра endTime, в функцию посылается его адрес. То есть, член, содержащий информацию о часовом поясе, не отбрасывается, поскольку здесь нет операции копирования, но функция Print по прежнему выводит только три члена объекта endTime - часы, минуты и секунды, так как в функции Print в выражении someTime.Write(); используется статическое связывание. Компилятор должен сгенерировать код на машинном языке для функции Print на этапе компиляции, но тип фактического параметра (Time или ExtTime) не известен до самого момента выполнения. Так как компилятор не может знать, какую из функций использовать - Time::Write или ExtTime::Write, он использует Time::Write, так как формальный параметр someTime имеет тип Time. Поэтому функция Print всегда выводит три значения - часы, минуты и секунды, независимо от типа фактического параметра. Язык C++ предоставляет очень простое решение этой проблемы: виртуальные функции. Виртуальные функции Виртуальные функции - это специальный вид член-функций класса. Для обычных функций связывание вызова функции с ее определением выполняется на этапе компиляции программы, а для виртуальной на этапе выполнения. Член-функция может быть объявлена виртуальной при следующих условиях: - класс, содержащий виртуальную функцию, будет базовым в иерархии порождения; - реализация функции зависит от класса и будет разной в каждом порожденном классе. Виртуальная функция - это функция, которая определяется в базовом классе, а в любом классе, порожденном из базового, может быть переопределена. Виртуальная функция вызывается только через указатель или ссылку на базовый класс. Какой экземпляр виртуальной функции будет вызываться, зависит от класса объекта, адрес которого содержит указатель или ссылка. Это и есть механизм динамического свзязывания. Член-функция определяется как виртуальная заданием ключевого слова virtual при объявлении функции в базовом классе. Только член-функция может быть объявлена виртуальной. Пример. Class Book {... public: ... virtual void Displау(); ... }; Член-функция Display - виртуальная, следовательно: 1. является общей для всей иерархии классов с базовым классом в основании иерархии; 2. определение функции различно в различных порожденных классах и не известно при объявлении базового класса. Обычно виртуальная функция не используется для объектов базового класса, т.е. не имеет определения. Чистой виртуальной функцией называется компонентная функция, которая имеет следующее определение: Virtual тип имя_функции (список формальных параметров)=0; Чистая виртуальная функция ничего не делает и недоступна для вызовов, еe назначение - служить основой для подменяющих ее функций в производных классах. Абстрактным называется класс, в котором есть хотя бы одна чистая (пустая) виртуальная функция. Абстрактный класс может использоваться только в качестве базового для производных классов. Механизм абстрактных классов разработан для представления общих понятий, которые в дальнейшем предполагается конкретизировать, при этом построение иерархии классов выполняется по следующей схеме: во главе иерархии стоит абстрактный базовый класс, используемый для наследования интерфейса, производные классы будут конкретизировать и реализовать этот интерфейс. В абстрактном классе объявлены чистые виртуальные функции, которые, по сути являются абстрактными методами. Пример. class Base {public: Base(); // конструктор по умолчанию Base(const Base&); // конструктор копирования virtual ~Base(); // виртуальный деструктор virtual void Show()=0; // чистая виртуальная функция ... //другие чистые виртуальные функции protected: // защищенные члены класса private: // часто остается пустым, чтобы // не мешать будущим разработкам }; //Base class Derived: virtual public Base { Derived(); // конструктор по умолчанию Derived(const Derived&); // конструктор копирования Derived(параметры); // конструктор с параметрами virtual ~Derived(); // виртуальный деструктор void Show();//переопределенная виртуальная функция ... //другие переопределенные виртуальные функции ... //другие перегруженные операции protected: //используется вместо private, //если ожидается наследование private: //используется для деталей реализации }; //Derived Объект абстрактного класса не может быть формальным параметром функции, однако формальным параметром может быть указатель на абстрактный класс. В этом случае появляется возможность передавать в вызываемую функцию в качестве фактического параметра значение указателя на производный объект, заменяя им указатель на абстрактный базовый класс. Таким образом, мы получаем полиморфные объекты. Полиморфная операция - операция, которая имеет много значений, зависящих от типа объекта, с которым операция связывается на этапе выполнения программы. Абстрактный метод может рассматриваться как обобщение переопределения. В обоих случаях поведение родительского класса изменяется для потомка. Для абстрактного метода, однако, поведение просто не определено. Любое поведение задается в производном классе. Одно из преимуществ абстрактного метода является чисто концептуальным: программист может мысленно наделить нужным действием абстракцию сколь угодно высокого уровня. Пример. Для геометрических фигур мы можем определить метод Draw, который их рисует: треугольник TTriangle, окружность TCircle, квадрат TSquare. Мы определим аналогичный метод и для абстрактного родительского класса TGraphObject. Однако такой метод не может выполнять полезную работу, поскольку в классе TGraphObject просто нет достаточной информации для рисования чего бы то ни было. Тем не менее присутствие метода Draw позволяет связать функциональность (рисование) только один раз с классом TGraphObject, а не вводить три независимые концепции для подклассов TTriangle, TCircle, TSquare. Имеется и вторая, более актуальная причина использования абстрактного метода. В объектно-ориентированных языках программирования со статическими типами данных, к которым относится и C++, программист может вызвать метод класса, только если компилятор может определить, что класс действительно имеет такой метод. Пример. Предположим, что программист хочет определить полиморфную переменную типа TGraphObject, которая будет в различные моменты времени содержать фигуры различного типа. Это допустимо для полиморфных объектов. Тем не менее компилятор разрешит использовать метод Draw для переменной, только если он сможет гарантировать, что в классе переменной имеется этот метод. Присоединение метода Draw к классу TGraphObject обеспечивает такую гарантию, даже если метод Draw для класса TGraphObject никогда не выполняется. Естественно, для того чтобы каждая фигура рисовалась по-своему, метод Draw должен быть виртуальным. Правила определения виртуальных функций. 1. Определение виртуальной функции в базовом классе. Если в базовом классе впервые объявлена виртуальная функция, то она должна быть либо чистой виртуальной, либо для нее должно быть задано определение. 2. Определение виртуальной функции в порожденном классе. Если в базовом классе объявлена чистая виртуальная функция, то в порожденном классе должно быть задано определение этой функции, либо функция должна быть вновь объявлена как чистая виртуальная. Язык не разрешает наследование по умолчанию чистой виртуальной функции. Если в базовом классе задано определение виртуальной функции, то в порожденном классе должно быть задано свое определение для функции, иначе будет использоваться определение функции базового класса. Виртуальная функция вызывается через ссылку или указатель на базовый класс, в котором она впервые объявлена. Интерпретация вызова виртуальной функции зависит от класса объекта, для которого этот вызов сделан. Пример. for (i=0; i<n; i++) lib[i]->Display(); Виртуальные деструкторы Деструктор класса может быть объявлен виртуальным. Практически каждый класс, имеющий виртуальную функцию, должен иметь виртуальный деструктор. Пример. for (i=0; i<n; i++) delete lib[i]; Чтобы эта структура работала, деструктор следует сделать виртуальным, так как в противном случае по выражению delete lib[i] вызовется деструктор класса Book. Пример. Виртуальный деструктор класса Book class Book {... public: Book(); Virtual ~Book(); }; Видимость виртуальных функций. Виртуальная функция вызывается через указатель или ссылку на базовый класс, следовательно; виртуальный вызов функции будет иметь такую видимость, с которой функция объявлена в базовом классе. Пример. Допустим из класса Event порождены два класса MouseEvent и KeyStroke. class Event {public: virtual int isType()=0; virtual int checkEvent()=0; protected: virtual int fromEvent()=0; ... }; class KeyStroke: public Event {public: int checkEvent(); int fromEvent(); ... }; class MouseEvent: public Event {public: int checkEvent(); int fromEvent(); ... }; Каждый виртуальный вызов функции checkEvent будет иметь видимость public, каждый виртуальный вызов функции fromEvent будет иметь видимость protected.
Date: 2016-05-13; view: 753; Нарушение авторских прав |