Архитектура Аудит Военная наука Иностранные языки Медицина Металлургия Метрология
Образование Политология Производство Психология Стандартизация Технологии


Объектно-ориентированный подход в программировании.



Объектно-ориентированный подход в программировании.

Инкапсуляция

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

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

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

Логическим продолжением инкапсуляции является сокрытие данных. Целями сокрытия данных и кода при инкапсуляции являются:

• предельная локализация изменений при необходимости таких изменений,

• прогнозируемость изменений (какие изменения в коде надо сделать для заданного изменения функциональности) и прогнозируемость последствий изменений.

Таким образом, комбинирование структуры данных с функциям и (действиям или методами), предназначенным и для манипулирования данным называется инкапсуляцией.

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

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

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

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

Наследование может быть простым или множественным.

Простое наследование

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

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

Множественное наследование

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

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

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

Таким образом, наследование выполняет в ООП несколько важных функций:

• моделирует концептуальную структуру предметной области;

• экономит описания, позволяя использовать их многократно для задания разных классов;

• обеспечивает пошаговое программирование больших систем путем многократной конкретизации классов.

Полиморфизм

Полиморфизм — один из четырёх важнейших механизмов объектно-ориентированного программирования. Он означает взаимозаменяемость объектов с одинаковым интерфейсом и позволяет одно и тоже имя использовать для решения нескольких технически разных задач.

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

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

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

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

Полиморфизм в языке С++ применим к функциям и операциям (имеются в виду операции типа +, =, [ ] и др.).

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

 

2. Объектно-ориентированные средства С++

2.1 Перегружаемые (overload) функции

Одна из ключевых черт полиморфизма в C++ - замещение или перегрузка функций.

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

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

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

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

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

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

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

2.2 Перегружаемые (overload) операторы

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

Иногда возникает потребность описывать и применять к созданным программистом типам данных операции, по смыслу эквивалентные уже имеющимся в языке. Классический пример — библиотека для работы с комплексными числами. Они, как и обычные числовые типы, поддерживают арифметические операции, и естественным было бы создать для данного типа операции «плюс», «минус», «умножить», «разделить», обозначив их теми же самыми знаками операций, что и для других числовых типов. Запрет на использование определённых в языке элементов вынуждает создавать множество функций с именами вида ComplexPlusComplex, IntegerPlusComplex, ComplexMinusFloat и так далее

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

Фундаментальное отличие C++ от C, делающее возможным применение принципов объектно-ориентированного программирования, состоит в том, что C++ не только позволяет замещать операторы и функции, но и активно подталкивает программиста к этому.

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

Существует ряд ограничений на замещение операторов по сравнению с замещением функций:

• оператор должен уже существовать в языке;

• нельзя переопределять действия встроенных в C++ операторов при работе со встроенным и типам и данных (например, нельзя изменить способ работы оператора «+» при сложении целых чисел);

• запрещено замещ ать операторы «.», «.*», «?: », «:: » и символы препроцессора « #».

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

Конструкторы и деструкторы

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

Функцией, обратной конструктору, является деструктор. Эта функция обычно вызывается при удалении объекта. Например, если при создании объекта для него динамически выделялась память, то при удалении объекта ее нужно освободить. Локальные объекты удаляются тогда, когда они выходят из области видимости. Глобальные объекты удаляются при завершении программы.

В языке C++ деструкторы имеют имена: « ~имя_класса». Как и конструктор, деструктор не возвращает никакого значения, но, в отличие от конструктора, не может быть вызван явно. Конструктор и деструктор не могут быть описаны в закрытой части класса.

Производные классы

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

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

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

class Parent {....};

class Child: [модификатор наследования] Parent {....};

 

При определении класса-потомка за его именем следует разделитель-двоеточие («: »), затем - необязательный модификатор наследования и имя родительского класса. Модификатор наследования определяет видимость наследуемых переменных и методов для пользователей и возможных потом ков самого класса-потомка. Другим и словами, он определяет, какие права доступа к переменным и методам класса-родителя будут переданы классу-потомку. При реализации наследования область видимости принадлежащих классу данных и методов можно определять выбором ключевого слова private (собственный), public (общедоступный) или protected (защищенный), которые могут произвольно чередоваться внутри описания класса. С двумя первыми модификаторами доступа мы уже знакомы - private описывает закрытые члены класса, доступ к которым имеют только методы-члены этого класса, public предназначен для описания общедоступных элементов, доступ к которым возможен из любого места в программе. Особый интерес представляют элементы, обладающие модификатором доступа protected.

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

Встраиваемые функции

Рассмотрим пример:

struct _3d

{

double x, y, z;

double mod()

{return sqrt (x*x + y*y +z*z); }

double projection(_3d r)

{return (x*r.x + y*r.y + z*r.z) / mod(); }

_3d operator + (_3d b);

};

_3d _3d:: operator + (_3d b)

{

_3d c;

c.x = x + b.x;

c.y = y + b.y;

c.z = z + b.z;

return c;

}

 

Обратим внимание на то, в каком месте описывается тело того или иного метода. Методы mod() и projection() описаны вместе со своими телами непосредственно внутри структуры. Однако можно поступить иначе: поместить прототипы метода внутрь структуры, а определение тела функции – вне структуры, как мы поступили с оператором « +». Первый способ используется для простых и коротких методов, которые в дальнейшем не предполагается изменять. Так поступают отчасти из-за того, что описания классов помещают обычно в файлы заголовков, включаемые затем в прикладную программу с помощью директивы #include. Кроме того, при этом способе машинные инструкции, генерируемые компилятором при обращении к этим функциям, непосредственно вставляются в оттранслированный текст. Это снижает затраты на их исполнение, поскольку выполнение таких методов не связано с вызовом функций и механизмом возврата, увеличивая в свою очередь размер исполняемого кода (то есть такие методы становятся встраиваемыми). Второй способ предпочтительнее для сложных методов. Объявленные таким об разом функции автоматически заменяются компилятором на вызовы подпрограмм, хотя при добавлении ключевого слова inline могут подставляться в текст, как и в первом случае.

Кроме представленного выше способа создания встраиваемых функций (записать тело метода непосредственно в структуре), есть еще один способ:

вставить спецификатор inline перед определением метода.

inline _3d _3d:: operator + (_3d b)

{

_3d c;

c.x = x + b.x;

c.y = y + b.y;

c.z = z + b.z;

return c;

}

Теперь оператор « +» станет встраиваемой функцией.

Встраиваемые функции действуют почти так же, как и макроопределения с параметрами, однако имеют ряд преимуществ. Во-первых, inline методы обеспечивают более стройный способ встраивания в программу короткой функции. Во-вторых, компилятор C++ гораздо лучше работает со встраиваемыми функциями, чем с макроопределениями. Важно понимать, что inline не является командой для компилятора, это, скорее, просьба сделать метод встраиваемым. Если по каким-то причинам (например, при наличии в теле функции операторов цикла, switch или goto) компилятор не выполнит запрос, то функция будет откомпилирована как не встраиваемая.

Присваивание объектов

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

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

class Pair

{

int a, *b;

public:

Pair operator = (Pair p)

{

a = p.a;

*b = *(p.b);

return *this;

}

...

};

пример с трехмерным вектором;

class _3d

{

double x, y, z;

public:

_3d ();

_3d (double initX, double initY, double initZ);

double mod()

{return sqrt (x*x + y*y +z*z); }

double projection(_3d r)

{return (x*r.x + y*r.y + z*r.z) / mod(); }

_3d operator + (_3d b);

_3d operator = (_3d b);

};

_3d _3d:: operator = (_3d b)

{

x = b.x;

y = b.y;

z = b.z;

return *this;

}

 

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

В качестве аргументов функций-операторов выступают операнды, а возвращаемое значение - результат применения оператора. В частности, для оператора « =» это необходимо, чтобы обеспечить возможность последовательного присваивания (a=b=c). Бинарные операторы имеют один аргумент - второй передается через указатель this. Унарные, соответственно, один - this.

 

 

Конструктор копирования

В C++ методом передачи параметров по умолчанию является передача объектов по значению. При передаче объекта в функцию появляется новый объект. Когда работа функции, которой был передан объект, завершается, то удаляется копия аргумента. Как формируется копия объекта и вызывается ли деструктор объекта, когда удаляется его копия?

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

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

Одним из способов обойти такого рода проблемы является создание особого типа конструкторов: конструкторов копирования. Конструктор копирования или конструктор копии позволяет точно определить порядок создания копии объекта.

Любой конструктор копирования имеет следующую форму.

имя_класса (const имя_класса & obj)

{

... // тело конструктора

}

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

class ClassName

{

public:

ClassName()

{

cout < < 'Работа конструктора \n';

}

ClassName(const ClassName& obj)

{

cout < < 'Работа конструктора копирования\n';

}

~ClassName()

{

cout < < 'Работа деструктора \n';

}

};

main()

{

ClassName c1; // вызов конструктора

ClassName c2 = c1; // вызов конструктора копирования

}

 

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

Модификаторы наследования

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

class Parent {....};

class Child: [модификатор наследования] Parent {....};

 

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

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

Модификатор protected используется тогда, когда необходимо, чтобы некоторые члены базового класса оставались закрытыми, но были бы доступны для производного класса. Модификатор protected эквивалентен private с единственным исключением: защищенные члены базового класса доступны для членов всех производных классов этого базового класса.

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

Совместимость типов

Наследование предъявляет свои требования к правилам совместимости типов.В добавление ко всему прочему, порожденный тип наследует совместимость со всеми типами предка. Эта расширенная совместимость имеет три формы:

• между экземплярами объектов;

• между указателями объектов;

• между формальными и фактическими параметрами.

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

Например,

Point APoint, *ptrPoint;

Circle ACircle, *ptrCircle;

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

APoint = ACircle;

Обратные присваивания неверны.

Родительскому объекту можно присваивать объект любого порожденного им класса.

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

1) источник должен полностью заполнять объект назначения;

2) порожденные типы содержат все, что содержат родительские типы, посредством наследования. Следовательно, порожденный тип будет иметь или тот же размер, или (обычно) размер больший, чем его предок, но никогда не меньший размер.

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

В предложениях присваивания будут копироваться только те поля, которые являются общими для обоих типов, то есть только поля предка.

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

Формальный параметр объекта заданного класса может использовать в качестве фактического параметра объект такого же класса или любого порожденного класса. Например,

void Proc (Point param)

Тогда фактические параметры могут иметь тип Point, Circle и любой другой порожденный от них тип.

 

Виртуальные функции.

Раннее и позднее связывание

Раннее или позднее связывание – это термины, относящиеся к этапу, на котором обращение к процедуре связывается с ее адресом. В случае раннего связывания адреса всех функций и процедур известны в тот момент, когда происходит компоновка программы.

Логика компилятора очень проста. Сначала он ищет метод с нужным именем, определенный внутри данного класса. Если метод с таким именем внутри класса не определен, то компилятор обращается к базовому классу и ищет этот метод там. Если найдет, то подставит в точки вызова адрес метода из родительского класса. Если не найдет, то поднимется по иерархическому дереву еще выше.

Методы, которые мы обсудили, называются статическими. Они являются статическим и в том смысле, что компилятор размещает их и разрешает ссылки на них во время компиляции. Это достаточно мощные средства для организации сложных программ. Однако они являются далеко не лучшим способом для обработки методов.

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

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

Специальный механизм разрешения ссылок на методы во время выполнения программы реализован в языке C++ при помощи виртуальных методов.

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

Различие между вызовом статического метода и вызовом виртуального метода - это различие между решением немедленным и отложенным, задержанным решением.

Абстрактный класс

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

Чисто виртуальной функцией называется виртуальная функция, указанная с инициализатором =0

Например:

virtual void F1(int) =0;

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

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

 

Дружественные функции.

5.1 “Дружественные” (friend) функции

Функция, объявленная в производном классе, может иметь доступ только к защищенным (protected) или общим (public) компонентам базового класса.

Функция, объявленная вне класса, может иметь доступ только к общим (public) компонентам класса и обращаться к ним по имени, уточненному именем объекта или указателя на объект.

Чтобы получить доступ к личным компонентам объектов некоторого класса Х в функции, не имеющей к ним доступа, эта функция должна быть объявлена дружественной в классе X:

class X

{ friend void Y:: fprv( int, char*);

/* Другие компоненты класса X */

}

Можно объявить все функции класса Y дружественными в классе X;

class Y;

class X

{ friend Y;

/* Другие компоненты класса X */

}

class Y

{ void fy1(int, int);

int fy2( char*, int);

/* Другие компоненты класса Y */

}

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

class XX

{ friend int printXX ( );

/* Другие компоненты класса ХХ */

}

Здесь функция printXX имеет доступ ко всем компонентам класса XX, независимо от закрепленного за ними уровня доступа.

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

5.2. Переопределение операторов с помощью дружественных функций.

Иногда для переопределения операций прибегают к услугам дружественных функций. Хотя дружественные функции и имеют доступ к приватным данным класса, но указатель this они не получают. Поэтому, например, с их помощью нельзя переопределить операцию '='. А другие унарные или бинарные операции такому переопределению поддаются легко, надо только передавать в дружественные функции на один параметр больше:

class Tpoint {

int x, y;

public

Tpoint(){x=0; y=0; } //конструктор по умолчанию

Tpoint(int xx, int yy){x=xx; y=yy; } //конструктор инициализации

void GeTpoint(int & xx, int & yy){xx=x; yy=y; } //опрос координат

friend Tpoint operator+(Tpoint P1, Tpoint P2);

};

Tpoint Tpoint:: operator+(Tpoint P1, Tpoint P2)

{ Tpoint q;

q.x=P1.x+P2.x; q.y=P1.y+P2.y; return q;

}

 

- Двуместные операции должны иметь два параметра, одноместные - один параметр, причем, если операция объявлена как компонента класса, то неявным первым операндом является экземпляр объекта (следовательно при определении двуместной операции будет задаваться один параметр, одноместная операция объявляется с пустым списком параметров). Если операция переопределяется вне класса (с описателем friend ), то для двуместной операции должны быть заданы два параметра, для одноместной операции - один параметр.

 

Шаблоны функций и классов

Шаблоны функций


Поделиться:



Популярное:

Последнее изменение этой страницы: 2016-07-14; Просмотров: 769; Нарушение авторского права страницы


lektsia.com 2007 - 2024 год. Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав! (0.171 с.)
Главная | Случайная страница | Обратная связь