3007

Основы объектно-ориентированного проектирования

Конспект

Информатика, кибернетика и программирование

Цель курса – освоение принципов объектно-ориентированного проектирования и методов объектно-ориентированного программирования с использованием языка С++. Если на компьютере установлена ОС Windows – в курсе рассматриваются примеры п...

Русский

2013-01-06

238.5 KB

14 чел.

Цель курса – освоение принципов объектно-ориентированного проектирования и методов объектно-ориентированного программирования с использованием языка С++.

“…Если на компьютере установлена ОС Windows – в курсе рассматриваются примеры программ, которые не предназначены специально для работы в этой ОС. Причина: программы для Windows по самой своей сути большие и сложные. При написании каждой такой программы для демонстрации возможностей языка С++ потребовалось бы написать тысячи строк исходного кода. Конечно, программирование на С++ под Windows позволяет пользоваться библиотеками классов, что существенно упрощает разработку приложений. Кроме этого, интерфейс любого приложения под Windows достаточно просто создать с помощью таких средств визуального программирования, как Visual C++ 5 или Borland C++ 5. Сердцевиной же любого профессионального приложения является программная реализация его идеи, а отнюдь не интерфейс, пусть даже самый что ни на есть дружественный” [7, стр. 6-7]. Поэтому будем рассматривать в курсе не создание пользовательского интерфейса в стиле Windows, а собственно язык программирования С++.

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

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

1.1. Основные понятия и определения

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

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

Сообщение определяется своим именем и всегда имеет адресата – кому оно адресовано. Кроме того, сообщение может иметь дополнительные аргументы, передаваемые адресату.

  1.  Пример:

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

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

  1.  Пример:

Рассмотрим тот же объект множество. Пусть aSet – конкретный экземпляр множества. Тогда посылку сообщений данному экземпляру можно проиллюстрировать следующим образом (рис. 1):

Рис. 1-1. Объекты – адресаты и сообщения

Можно ввести следующие определения (используя и терминологию языка С++):

Объект – инкапсулированная абстракция, которая включает в себя информацию о состоянии и четко определенное множество протокола доступа (поведение).

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

1.2. Процесс разработки ПО

В основе деятельности по созданию и использованию программного обеспечения (ПО) лежит понятие его жизненного цикла (ЖЦ). ЖЦ является моделью создания и использования ПО, отражающей ее различные состояния, начиная с момента возникновения необходимости в данном программном изделии и заканчивая моментом его полного выхода из употребления у всех без исключения пользователей.

Традиционно выделяют следующие основные этапы ЖЦ ПО:

анализ требований,

проектирование,

кодирование (программирование),

тестирование и отладка,

эксплуатация и сопровождение.

Существующие модели ЖЦ определяют порядок выполнения этапов в ходе разработки, а также критерии перехода от этапа к этапу.

Главная особенность индустрии ПО состоит в концентрации сложности на начальных этапах ЖЦ (анализ, проектирование) при относительно невысокой сложности и трудоемкости последующих этапов. Более того, нерешенные вопросы и ошибки, допущенные на этапах анализа и проектирования, порождают на последующих этапах трудные, часто неразрешимые проблемы и, в конечном счете, приводят к неуспеху всего проекта. [Калянов Г.Н. CASE-технологии. Консалтинг при автоматизации бизнес процессов. 2-е изд. – М.: Горячая линия – Телеком, 2000. – 320 с., ил. Стр. 20 - 22]

Первые четыре этапа определяют ЖЦ разработки ПО и (более или менее подробно) рассматриваются в данном курсе.

В [11, стр. 36-37] дается разграничение понятий метода и методологии. Метод – это последовательный процесс создания моделей, которые описывают вполне определенными средствами различные стороны разрабатываемой программной системы. Методология – это совокупность методов, применяемых в ЖЦ разработки ПО и объединенных одним общим философским подходом. Методы важны, так как они упорядочивают процесс создания сложных программных систем.

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

  •  метод структурного проектирования сверху вниз,
    •  метод потоков данных,
    •  метод объектно-ориентированного проектирования.

Наибольшее распространение получило структурное проектирование по методу сверху вниз. Структурное проектирование использует следующий подход, определяемый топологией традиционных языков высокого уровня: применяется алгоритмическая декомпозиция для разбиения большой задачи на более мелкие. И сейчас значение структурного подхода осталось прежним, но, как замечает Стейн, «оказалось, что структурный подход не работает, если объем программы превышает приблизительно 100 000 строк» (цит. по [11, стр. 36]).

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

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

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

Например, для упомянутого ранее класса Рациональная дробь на данном этапе можно ввести следующие уточнения: состояние класса – числитель и знаменатель дроби, знаменатель обязательно отличен от нуля и всегда положителен (знак дроби определяет ее числитель); числитель и знаменатель дроби всегда представлены в виде взаимно простых чисел. Поведение класса – набор необходимых операций из постановки задачи (математические операции, ввод-вывод, сравнения и т.п.), представленных в виде привычных знаков арифметических операций (+, - и т.п.; это возможно при использовании языка программирования С++). Теперь можно независимо разрабатывать класс Рациональная дробь и ПП.

ОО Программирование и отладка – используются конкретные инструментальные средства; в нашем распоряжении – Borland С++ или Visual C++ (в зависимости от лабораторного класса). Конечно, на этом этапе при отладке ПП должна быть доступна какая-то реализованная версия класса Рациональная дробь. Изменения и дополнения, которые могут быть внесены в класс Рациональная дробь, не должны требовать перепрограммирования ПП. Модификация ПП и изменения в предметной области могут привести к появлению дополнительных требований к классу; опять же новые возможности класса не должны оказать серьезное влияние на старые уже существующие версии ПП.

Определение объектно-ориентированного языка программирования (ООЯП)

ООЯП – это язык программирования, обладающий набором определенных свойств. Главными свойствами ООЯП являются абстракция, инкапсуляция, наследование, полиморфизм.

Абстракция – это свойство, позволяющее создавать новые типы данных, выделяя общие абстрактные свойства этих типов.

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

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

Полиморфизм – это свойство, которое позволяет одно и то же имя использовать для решения двух или более схожих, но технически разных задач. Например, в не ОО языках программирования нахождение абсолютной величины числа требует нескольких различных функций – в зависимости от типов аргумента и результата (например, в С – это abs() для данных типа int, labs() для данных типа long и fabs() для данных типа double). В ООЯП каждая из этих функций может быть названа abs(). Тип данных, который используется при вызове функции, определяет, какая конкретно версия функции действительно выполняется.

  1.  Пример использования ООП (объектно-ориентированного проектирования)

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

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

Отсюда – этапы разработки: спроектировать и реализовать класс Rational – рациональная дробь; спроектировать и реализовать основную задачу; провести исследования решения; в случае необходимости – модифицировать класс Rational.

2. Разработка класса в ООП

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

2.1. Основные этапы разработки класса

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

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

Рассмотрим класс “рациональная дробь” –  Rational [‘рэшенел].

Состояние класса: два поля типа “целое”, с именами num (от numerator [‘нью:мерэйте] –  числитель) и den (от denominator [ди’номенэйте] –  знаменатель. Пока ограничиваемся диапазоном представления в стандартных типах. Дополнительные требования: знаменатель не должен быть равен нулю, ни при каких условиях; знаменатель всегда положителен, знак дроби определяется знаком числителя; поля класса не должны быть доступны извне класса непредусмотренными классом способами.

Методы класса: традиционные арифметические операции (сложение, умножение и т.п.), ввод/вывод; кроме того, потребуются вспомогательные операции для выполнения арифметических операций –  типа сокращения дроби и т.п.

2.2. Определение класса

Представление класса на языке программирования С++.

Для определения класса предусмотрено специальное ключевое слово class, но можно использовать и традиционное struct.

Синтаксис определения класса приведен на рис. 2-1.

Class имя_класса{

уровень_видимости:

описания_полей_класса

прототипы_функций-методов_класса

уровень_видимости:

. . .

};

struct имя_класса{

уровень_видимости:

описания_полей_класса

прототипы_функций-методов_класса

уровень_видимости:

   . . .

};

Рис. 2-1. Определение класса

Уровень_видимости задается одним из трех ключевых слов:

  •  private [‘прайвит] –  определяет закрытую часть класса, не доступную извне класса;
    •  protected [прэ’тэктид] – пока для нас аналогичен private; различия между ними проявляются при использовании наследования;
    •  public [‘паблик] –  определяет открытую часть класса, видимую и доступную извне класса.

Определение класса можно проиллюстрировать следующим образом (рис. 2-2):

Рис. 2-2. Уровни видимости класса

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

Описания_полей_класса и прототипы_функций определяются в соответствии с обычными правилами С++ (рис. 2-3).

class X{

private:

int a1;

void f1();

protected:

char a2;

public:

double a3;

int f3();

};

struct X{

private:

int a1;

void f1();

protected:

char a2;

public:

double a3;

int f3();

};

Рис. 2-3. Пример определения класса

Объявляем экземпляр нового типа данных X – в соответствии с обычными правилами (независимо от того, определен класс с помощью struct или class):

 X obj;

Тогда обращения:

obj.a1, obj.a2, obj.f1() – вызовут сообщения об ошибке (члены класса a1, a2 и f1() не видны (не доступны) извне класса;

obj.a3, obj.f3()– корректны.

Внутри функций-методов класса f1()и f3()можно без опасений использовать все имена: a1, a2, a3, f1()и f3().

Порядок следования ключевых слов, определяющих уровень видимости, произволен; они могут появляться неоднократно или отсутствовать в определении класса. Если в начале определения класса отсутствует уровень видимости, тогда для class предполагается private, а для structpublic (рис. 2-4).

определение 

class X{

 int a1;

void f1();

. . .

};

эквивалентно 

class X{

private:

int a1;

void f1();

. . .

};

a)

определение 

struct X{

 int a1;

void f1();

. . .

};

эквивалентно 

struct X{

public:

int a1;

void f1();

. . .

};

b)

Рис. 2-4. Правила умолчания для class (a) и struct (b)

В дальнейшем для определения класса будем использовать ключевое слово class.

  1.  Рекомендации по поводу использования уровней видимости при определении класса

Члены-данные класса, определяющие его состояние, как правило, помещаются в private- или protected- область класса – они не должны быть непосредственно доступны извне класса. Доступ к состоянию класса должен определяться только интерфейсом класса.

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

2.3. Методы класса

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

По функциональному назначению методы класса делятся на следующие категории:

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

По отношению к классу методы делятся на следующие две категории:

  •  функция-член класса – функция, принадлежащая самому классу и не существующая вне класса; прототипы функций-членов класса включены в определение класса;
  •  функция-друг класса – внешняя по отношению к классу функция, которая может существовать вне класса, но имеет доступ к закрытой (и защищенной) части класса. Прототип функции-друга класса также включается в определение класса, но начинается специальным ключевым словом friend.

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

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

Как упоминалось выше, конструкторы служат для инициализации экземпляров класса в момент их создания.

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

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

По тому, каким образом конструктор инициализирует состояние класса, конструкторы определяются как инициализирующие и копирующий.

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

Копирующий конструктор инициализирует состояние класса значением другого экземпляра этого класса (создает копию существующего экземпляра класса). В списке параметров указывается единственный параметр, имеющий тип «ссылка на экземпляр класса».

По тому, кто определяет конструкторы, последние делятся на конструкторы по умолчанию (не требуют какого-либо упоминания в определении класса) и явно определенные программистом.

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

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

В определении конструкторов отсутствует тип возвращаемого значения (конструктор ничего не возвращает); имя конструктора совпадает с именем класса; в классе может быть определено несколько конструкторов.

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

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

В определении деструктора также отсутствует тип возвращаемого значения; имя деструктора также совпадает с именем класса, но начинается символом ~.

Правила записи прототипов конструкторов разных типов и деструктора приведены на рис. 2-5.

Тип метода

Прототип

Примечания

Пустой конструктор

имя_класса();

Инициализирует состояние предопределенными значениями

Инициализирующие конструкторы

имя_класса(тип параметр, ...);

Тип – любой; инициализирует состояние значениями, заданными в списке аргументов

Копирующий конструктор

имя_класса(const имя_класса & параметр);

Инициализирует состояние значением указанного в списке аргументов экземпляра данного класса; модификатор const указывает, что для инициализации экземпляра класса можно использовать константы

Деструктор

~ имя_класса ();

Рис. 2-5. Правила записи прототипов конструкторов и деструктора

Пример определения класса Рациональная дробь (Rational) приведен ниже (рис. 2-6).

class Rational{

private:

 int num, den;  // состояние класса – числитель и знаменатель дроби

 int gcd() const; // метод класса – нахождение наибольшего общего делителя

 void reduce();  // метод класса – сокращение дроби

 void correct();  // метод класса – коррекция дроби

protected:

 /* отсутствует: можно совсем не включать данную часть класса, вместе с ключевым
 словом

*/

public:

/* Конструкторы класса */

 Rational();      // пустой конструктор

 Rational(int num);    // инициализирующий конструктор

 Rational(int num, int den); // инициализирующий конструктор с 2 аргументами

/* Деструктор класса */

~Rational();

/* Методы класса: селекторы */

 void print()const;  // вывод значения дроби в поток

 Rational add(const Rational &opd)const;  // сложение дробей

/* Модификатор */

void assign(int x, int y);  // присваивание дроби нового значения

};

Рис. 2-6. Пример определения класса Рациональная дробь

2.5. Использование класса

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

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

имя_класса    имя_объекта;    // элементный объект

имя_класса    имя_объекта [количество]; // массив объектов

имя_класса  *имя_объекта;    // указатель на объект

Например:

Rational a;

Rational b[4];

Rational *ptr;

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

а) элементные объекты

имя_класса имя_объекта,     // пустой конструктор

  имя_объекта(значение),   // одноаргументный инициализирующий

// конструктор

  имя_объекта(знач1, знач2),  // 2-х аргументный инициализирующий

// конструктор

  имя_объекта1(имя_объекта2); // копирующий конструктор

Например, для класса Rational это будет выглядеть так:

Rational a1,   // пустой конструктор

  a2(2),  // одноаргументный инициализирующий конструктор

   a3(2, 5); // 2-х аргументный инициализирующий конструктор

Rational b(a1);  // копирующий конструктор; эквивалентная запись –

// Rational b = a1;

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

имя_класса    имя_объекта[количество] = {знач1, знач2, . . .};

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

Например, для класса Rational это выглядит так:

Rational mas[4] = {Rational(),  // пустой конструктор

Rational(2),  // одноаргументный конструктор

Rational(3, 8), // 2-х аргументный конструктор

Rational(a1)  // копирующий конструктор

 };  

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

имя_класса  *имя_объекта;

имя_объекта = new имя_класса;  – выделяется память под один экземпляр класса; будет вызван пустой конструктор

имя_объекта = new имя_класса(арг, . . .);   – также выделяется память под один экземпляр; для его инициализации будет вызван указанный конструктор

имя_объекта = new имя_класса [количество];   – выделяется память под массив экземпляров; для каждого экземпляра массива будет вызван пустой конструктор

Примеры для класса Rational:

Rational *p1, *p2, *p3;

p1 = new Rational;   // инициализация пустым конструктором

p2 = new Rational(2, 3); // инициализация 2-х аргументным конструктором

p3 = new Rational[5];  // массив из 5 элементов; для каждого элемента вызывается

// пустой конструктор

2.5. Реализация класса

Реализация класса предполагает разработку и написание на языке всех функций – методов  класса. Каждая функция должна быть определена (и только один раз). Определение функции имеет обычный вид:

тип_результата   имя_функции (тип пар1, ) // заголовок функции

{

 тело_функции

}

Функцию можно определить со спецификатором inline. Такие функции называются встроенными:

inline тип_результата   имя_функции (тип пар1, )

{

 тело_функции

}

Спецификатор inline указывает компилятору, что он должен пытаться каждый раз генерировать в месте вызова код, соответствующий указанной функции, а не создавать отдельно (один раз) код функции и затем вызывать его, используя обычный механизм вызова. Отличия в использовании обычных и встроенных функций приведены на рис. 2-7.

Рис. 2-7. Использование обычных и встроенных функций

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

тип_результата   имя_класса::имя_функции (тип пар1, )

{

 тело_функции

}

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

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

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

this->имя_члена

Если нет никаких неясностей и неопределенностей, имя_класса и/или this-> могут быть опущены.

  1.  Пример: реализация класса Rational

Рассмотрим реализацию класса Rational, определенного выше.

class Rational{

private:

int num, den;  // состояние класса – числитель и знаменатель дроби

 int gcd() const; // метод класса – нахождение наибольшего общего делителя

 void reduce();  // метод класса – сокращение дроби

 void correct();  // метод класса – коррекция дроби

protected:

public:

/* Конструкторы класса: пустой; инициализирует дробь значением 0 */

 Rational(){num = 0; den = 1; }

/* Инициализирующий с 1 аргументом; инициализирует дробь целым значением */

 Rational(int num){Rational::num = num; den = 1; }

/* Инициализирующий с 2 аргументами; инициализирует дробь заданным значением */

 Rational(int num, int den) {num = n; den = d; correct(); }

/* Деструктор класса */

~Rational(){}

/* Методы класса: селекторы */

 void print() const;

Rational add(const Rational &opd) const;

/* Модификатор */

void assign(int x, int y);

};

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

  1.  Реализация методов класса

inline void Rational::correct()

{

if(!den)

 den = 1;

if(den < 0)

 num = -num, den = -den;

}

inline void Rational::assign(int x, int y)

{

num = x;

den = y;

correct();

}

Видно, что две функции – двух аргументный конструктор и assign – имеют одинаковые коды; но это функционально разные функции: конструктор будет вызываться при объявлении и инициализации данных типа Rational, тогда как assign можно вызывать неоднократно – каждый раз, когда с помощью присваивания нужно изменить значение уже существующего экземпляра класса. Отличие такое же, как и в случае использования базовых типов: int x = 1; ... x = 1; ...

// Нахождение наибольшего общего делителя для числителя и знаменателя дроби.

// Известно, что знаменатель дроби всегда > 0

int Rational::gcd() const

{

int n = abs(num), d = den, r;

while(r = n % d) // вычисляется остаток от деления и сравнивается с 0

 n = d, d = r; // переопределяются делимое и делитель

 return r;

}

// Сокращение дроби

void Rational::reduce()

{

int div = gcd();

num /= div;

den /= div;

}

// Сложение дробей

Rational Rational::add(const Rational &opd) const

{

Rational temp;

temp.num = num * opd.den + den * opd.num;

temp.den = den * opd.den;

temp.reduce();

return temp;

}

// Вывод значения дроби в выходной поток

void Rational::print() const

{

cout << num;

if(den > 1)

 cout << ’/’<< den;

}

2.6. Использование класса

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

Простые переменные:

Rational a,   /* пустой конструктор; конструкция Rational a() определяет
     обычную функцию, возвращающую значение типа
Rational */

  d(5),  /* одно аргументный инициализирующий конструктор */

  b(3,8); /* двух аргументный инициализирующий конструктор */

Возможна и традиционная инициализация экземпляров класса:

Rational c = 8,  /* В результате будет создана дробь со значением 8/1 */

p = Rational(3,8); /* Так как при классической инициализации требуются значения соответствующего типа, а в языке не определены константы типа Rational, нужно построить такую константу, явно вызвав конструктор класса */

Массивы:

Rational x[3],  /* Используется пустой конструктор для создания каждого элемента
     массива
*/

y[] = {2, 1, Rational(3,8)}; /* Обычный синтаксис при инициализации массива, обязательно используются значения соответствующего типа */

Использование свободной памяти

Rational *ptr1, *ptr2;

ptr1 = new Rational(1,3); /* Классическое использование операции new, в которой указывается имя нового типа; при этом возможна сразу и инициализация выделенной области памяти за счет работы соответствующего конструктора */

ptr2 = new Rational[4]; /* Если выделяется память под массив, работает только пустой конструктор; инициализация памяти не выполняется */

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

  1.  Пример использования класса

main()

{

Rational a(2), b[3], x, y;

 const Rational c(5,8);

// Вывод значения дроби a

 a.print(); cout << endl;

// Вывод значения элемента массива b

 b[1].print(); cout << endl;

// Сложение значений дробей a и c

 x = a.add(c);

// Вывод результата сложения

 x.print(); cout << endl;

// Сложение дроби x с дробью 3/5 и вывод результата

x.add(Rational(3,5)).print(); cout << endl;

/* Для свободной памяти */

 Rational *ptr;

ptr = new Rational(3,8);

(*ptr).print(); cout << endl; /* Возможна и запись ptr->print(); */

}

Ошибки:

a.gcd()

a.reduce()

и т.п.

Еще пример – решение основной задачи (система двух уравнений с двумя неизвестными). Предполагается, что для класса Rational определены все арифметические операции: сложения (add), вычитания (sub), умножения (mul) и деления (div).

Решить систему вида:

 

Значения коэффициентов системы приведены в таблице:

a

b

c

d

e

f

2

3

-1

5

2

3

Решение имеет вид:

определитель системы det = a * e - d * b;

x = (c * e - b * f) / det; y = (a * f - d * c) / det;

Чтобы умножить a на e, нужно экземпляру a послать сообщение: “умножь себя (свое значение) на e”: a.mul(e);

main()

{

Rational a(2), b(3), c(-1), d(5), e(2), f(3), x, y;

Rational det;

det = (a.mul(e)).sub(d.mul(b));

x = (c.mul(e)).sub(b.mul(f)).div(det);

y = (a.mul(f)).sub(d.mul(c)).div(det);

x.print(); cout << ’,’; y.print(); cout << endl;

}

  1.  Перегрузка

3.1. Перегрузка функций

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

Примеры:

void f(int);

void f(char);

void f(long);

void f(float, int);

void f(int, int, int);

Функции с параметрами, заданными по умолчанию - пример перегрузки функций. Важно! перегружаемые функции не должны иметь совпадающие списки параметров (в том числе и при использовании параметров по умолчанию).

Вместе с приведенными выше примерами не может быть задана функция

void f(long, float = 0, char = '*');

но можно задать такую функцию:

void f(float, long = 0, char = '*');

3.2. Перегрузка операторов

Правила перегрузки операторов: только унарные и бинарные, почти все операторы; составное имя функции – operator знак_операции. Приоритет оператора, правило ассоциативности и количество операндов изменить нельзя!

Пример перегрузки бинарного оператора (сложение для класса Rational):

class Rational{

public:

...

Rational operator +(Rational r);

...

}

Rational Rational::operator +(Rational r)

{

Rational tmp;

tmp.num = num*r.den + den*r.num;

tmp.den = den*r.den;

tmp.reduce();

return tmp;

}

Унарные операторы перегружаются аналогично, за исключением ++ и (они могут иметь префиксную и постфиксную формы записи):

Префиксная ++:

Rational operator ++();

Постфиксная ++:

Rational operator ++(int); 

Использование перегруженных операторов:

Rational a(1,5), b(2,7), c;

c = a + b;  // классическая запись: c = a.operator +(b);

a++;   // классическая запись: a.operator ++(int);

++a;   // классическая запись: a.operator ++();

3.3. Выбор перегруженной функции

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

Правила сравнения:

  1.  Точные совпадения
  2.  Расширения
  3.  Стандартные преобразования
  4.  Преобразования, требующие временные переменные
  5.  Преобразования, определенные пользователем

Преобразования, требующие временные переменные: параметр объявлен как ссылка, а аргумент требует преобразования (например, преобразование из float в int&) или задан выражением, значение которого не может быть изменено.

  1.  Примеры

void print(int);

void print(const char *);

void print(double);

void print(long);

void print(char);

char c; int i; short s; float f;

print(c);   // правило 1; вызывается print(char)

print(i);   // правило 1; вызывается print(int)

print(s);   // правило 2; вызывается print(int)

print(f);   // правило 2; вызывается print(double)

print('a');  // правило 1; вызывается print(char)

print(49);  // правило 1; вызывается print(int)

print("a");  // правило 1; вызывается print(const char *)

  1.  Пример с ошибками

void f(int, float);

void f(float, int);

Вызов, который приведет к генерации сообщения об ошибке (неоднозначный выбор):

f(1.5, 1.5);

3.5. Друзья класса

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

Функция становится другом после ее объявления в классе с использованием спецификатора friend, например:

Определение

Реализация

а) глобальная функция

class X{

  ...

friend void f();

public:

void fx();

...

}

void f()

{

  ...

}

б) функция – член класса

class Y{         

...          

friend void X::fx();    

...          

};

void X::fx()

{

...

}

в) класс

class Z{

...

friend class Y;

 ...

};

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

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

Различия между членами и друзьями класса:

Функция-член

Функция-друг

class Rational{

public:

  void print();

  ...

};

void Rational::print()

{

  cout << num;

  if(den != 1)

     cout << '/' << den;

}

...

Rational x(1,5);

x.print();

class Rational{

public:

  friend void print(Rational r);

  ...

};

void print(Rational r)

{

  cout << r.num;

  if(r.den != 1)

     cout << '/' << r.den;

}

...

Rational x(1,5);

print(x);

Перегруженные операторы – друзья класса:

Бинарный оператор

Унарный оператор (префиксный и постфиксный)

Объявление

friend тип operator знак_оп(op1, op2)

friend тип operator знак_оп(op1)

friend тип operator знак_оп(op1, int)

Реализация

тип operator знак_оп(тип op1, тип op2)

{ ... }

тип operator знак_оп(тип op1)

{ ... }

тип operator знак_оп(тип op1, int)

{ ... }

Использование

op1 знак_оп op2

эквивалентно: operator знак_оп(op1, op2)

знак_оп op1

op1 знак_оп

Пример перегрузки оператора записи в поток для класса Rational:

class Rational {

friend ostream& operator <<(ostream&, Rational);

...

};

ostream& operator <<(ostream& os, Rational r)

{

os << r.num;

if(r.den == 1)

 os << / << r.den;

 return os;

}

3.6. Друзья или члены

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

Общее:

имеют доступ к закрытой части класса,

хотя бы один аргумент – экземпляр класса

Различие:

член

друг

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

все n параметров в списке параметров

неявный параметр – адресат сообщения; доступен через this

все параметры равноправны; адресата сообщения нет; this не определено

адресат сообщения (первый аргумент) – обязательно экземпляр класса

порядок и типы аргументов определяются объявлением функции

Rational x(1,3);

x + 1 - все в порядке

1 + x - ошибка!

Rational x(1,3);

x + 1 - все в порядке

1 + x - все в порядке

Функции-члены класса:

конструкторы, деструкторы, виртуальные функции;

операции, требующие в качестве операндов основных типов lvalue (например, =, +=, ++ и т.д.)

операции, изменяющие состояние объекта

Функции-друзья класса:

операции, требующие неявного преобразования операндов (например, +, - и т.д.)

операции, первый операнд которых не соответствует типу экземпляра класса (например, << и >>).

При прочих равных условиях лучше выбирать функции-члены класса.

3.6. Преобразования типа

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

Из существующего типа в новый – с помощью одноаргументного конструктора.

Пример для класса Rational:

Rational x = Rational(23); // явный вызов

Rational y = 23;     // неявный вызов

Возможны любые использования:

Rational a(1,2), b(1), c;

c = a + b;

c = a + 1;  // эквивалентно c = a + Rational(1);

c = 2 + a;  // эквивалентно c = Rational(2) + a;

Из нового типа в существующий - с помощью перегрузки оператора преобразования типа

Обязательно функция-член класса.

Прототип:  operator имя_типа ();

Реализация:  имя_класса::operator имя_типа() { ... }

Использование: неявно при вычислении выражений или явно с помощью обычного оператора преобразования типа: имя_типа(выражение).

Пример для класса Rational:

class Rational{

public:

...

operator float() { return (float) num / den; }

 ...

};

Использование:

Rational x(3,2);

float f = x;

Еще пример:

while( cin >> n)

cout << n;

Возможные неприятности:

Если в классе Rational есть одноаргументный конструктор (преобразование int в Rational) и в нем будет перегружена операция преобразования типа для int (преобразование Rational в int), тогда конструкция:

Rational a(1,2);

... a + 1 ...

вызовет сообщение об ошибке: два преобразования типа, определенные пользователем; что выбрать: int + int или Rational + Rational?

4. Классы, использующие свободную память

Причины, по которым приходится создавать классы с динамическим управлением памятью. Пример такого класса: функция, заданная таблично. Состояние класса включает в себя информацию о размере таблицы (количество узлов в задании функции) и массивы для хранения значений аргумента (х) и соответствующих значений функции (y). Массивы определяются через соответствующие указатели, размер таблицы определяет требуемый объем памяти. Кто выделит память? - конструктор. Пример разработки соответствующего класса.

class Function{

private:

float * Ptrx, * yPtr;

int size;

public:

 Function();   // пустой конструктор

Function(int sz); // инициализирующий конструктор

 ~Function();

. . .

};

Function::Function()

{

size = 0;

xPtr = yPtr = NULL;

}

Function::Function(int sz)

{

size = sz;

xPtr = new float[size];

yPtr = new float[size];

}

Function::~Function()

{

delete [] xPtr;

 delete [] yPtr;

}

4.1. Возникающие проблемы

Если в прикладной программе в некотором внутреннем блоке создается переменная типа “Указатель на класс” и ей присваивается значение некоторой другой – глобальной переменной: для классов (структур) определена операция присваивания - побайтное копирование:

Function f1[5];    f1   5        f2    5

. . .

{

Function f2;

f2 = f1;

 . . .

}

В момент выхода из блока переменная f2 разрушается – для нее работает деструктор класса. В результате будет разрушена и переменная f1!

Результат не изменится, если во внутреннем блоке вместо присваивания использовать инициализацию (в этом случае будет работать копирующий конструктор по умолчанию – тоже побайтное копирование):

{

Function f2 = f1;

 . . .

}

4.2. Решение проблемы

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

Копирующий конструктор (объявление в определении класса):

. . .

Function(const Function&);

. . .

Копирующий конструктор должен:

Выделить новую память для создаваемого экземпляра класса,

Скопировать в нее состояние существующего экземпляра класса.

Реализация:

Function::Function(const Function &f)

{

size = f.size;

xPtr = new float[size];

yPtr = new float[size];

for(int i = 0; i < size; i++){

 xPtr[i] = f.xPtr[i];

 yPtr[i] = f.yPtr[i];

 }

}

Перегруженный оператор присваивания (объявление в определении класса; функция-член класса):

. . .

Function& operator =(const Function&);

. . .

Перегруженный оператор присваивания должен:

Освободить память, занимаемую экземпляром класса – адресата оператора присваивания (указанного слева от присваивания),

Выделить новую память для нового значения экземпляра класса – адресата,

Скопировать в нее значение экземпляра класса, указанного справа от присваивания (параметр оператора присваивания),

Проверить возможность записи присваивания типа x = x.

Реализация:

Function& Function::operator =(const Function &f)

{

if(this != &f){  // проверка ситуации x = x

 ;

 delete [] xPtr;

 delete [] yPtr;

 xPtr = yPtr = NULL;

 if(size = f.size){ // проверка ситуации, когда у объекта f нет памяти

  xPtr = new float[size];

 yPtr = new float[size];

   for(int i = 0; i < size; i++){

   xPtr[i] = f.xPtr[i];

  yPtr[i] = f.yPtr[i];

   }

 }

}

 return *this;

}

4.3. Когда используются присваивание и копирующий конструктор

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

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

  1.  Явное создание временных экземпляров класса с использованием копирующего конструктора – при объявлении в программе экземпляров класса с инициализацией их другим экземпляром класса, например:

Function f1(5);  // работает инициализирующий конструктор

Function f2(f1),  // работает копирующий конструктор

  f3 = f1; // работает копирующий конструктор

  1.  Неявное создание временных экземпляров класса с использованием копирующего конструктора – при использовании функции, аргументами которой являются экземпляры класса (не ссылки и не указатели) и которая возвращает в качестве результата значение экземпляра класса, например:

Function myfunc(Function f)

{

Function tmp;

. . .

return tmp;

}

Если в программе есть такая функция и выполняется действие f1 = myfunc(f2), тогда:

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

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

параметры функции и объявленная в ней временная переменная tmp разрушаются (работает деструктор);

и, наконец, работает перегруженный оператор присваивания.

4.4. Использование модификатора const

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

class X{

private:

int n;

public:

X(int a = 0){n = a;}

int getval(){return n;}

X& setval(int a){n = a; return *this;}

};

Если в программе есть:

X ob1(5);

const X ob2(5);

тогда:

ob1.setval(4); все в порядке;

ob2.setval(4); ошибка компиляции, т.к. значение константы не может быть изменено.

int k = ob1.getval(); все в порядке;

int l = ob2.getval(); ошибка компиляции (непредусмотренное использование константного объекта ob2), т.к. компилятор ничего не знает о том, что метод getval() не изменияет состояние адресата, а адресатом является константа.

Если изменить класс X следующим образом:

class X{

private:

int n;

public:

X(int a = 0){n = a;}

int getval() const {return n;}

X& setval(int a){n = a; return *this;}

};

Тогда все будет в порядке, и вызов int l = ob2.getval(); вернет корректное значение.

Если реализация метода приводится вне определения класса, тогда модификатор const должен появиться в двух местах: в прототипе функции-члена класса и в определении функции:

class X{

private:

int n;

public:

X(int a = 0){n = a;}

int getval() const;

X& setval(int a){n = a; return *this;}

};

int X::getval() const

{

return n;

}

Статические члены класса

Типы отношений между классами

Контейнерные классы: определение

Контейнерные классы: реализация методов

Контейнерные классы: реализация конструкторов

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

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

Отсюда:

     базовый класс

собственные

собственные

Состояние - члены-данные

набор методов

     производный класс

от базового класса

от базового класса

собственные

собственные

Состояние - члены-данные

набор методов

В производном классе имена методов (и членов - данных) могут совпадать с именами из базового класса.

Простое наследование: правила определения производного класса

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

Первая проблема решается уровнем видимости членов в базовом классе:

class B{

private:

 // закрытая часть класса; не доступна никому, кроме методов класса (в том числе

 // не доступна и из производного класса)

protected:

// защищенная часть класса; доступна из методов базового и производного класса

 // (но не доступна извне класса)

public:

// открытая часть класса; доступна везде

};

Вторая проблема решается способом наследования:

class D: тип_наследования B{

. . .

};

тип_наследования: одно из private, protected, public.

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

тип наследования

уровень видимости в базовом классе

private

protected

public

protected

private

protected

protected

public

private

protected

public

Примеры.

Реализация конструкторов производного класса. Примеры.

Пример производного класса

Пример использования простого наследования: класс “точка” - базовый для класса “окружность”. Методы в классе “точка”: расстояние между двумя точками, вывод координат точки. Для класса окружность: свои дополнительные методы, отсутствовавшие в классе “точка” (пересечение окружностей); полностью наследуется метод - расстояние между точками (центрами двух окружностей) и требуется переопределить метод вывода окружности.

class Point{

protected:

int x, y;

public:

// конструкторы

Point():x(0), y(0){}

 Point(int x0):x(x0),y(x0){}

Point(int x0, int y0):x(x0), y(y0){}

Point(const Point &p):x(p.x), y(p.y){}

void print();

float distance(Point p);

};

Реализация класса:

void Point::print()

{

cout << '(' << p.x << ", " << p.y << ')';

}

float Point::distance(Point p)

{

float dx = x - p.x;

float dy = y - p.y;

return sqrt(dx * dx + dy * dy);

}

Теперь - производный класс Окружность:

class Circle: public Point{

private:

int rad;

public:

// конструкторы

Circle():rad(0){} // Вызывается пустой конструктор базового класса

 Circle(int r):rad(r){}

Circle(int x, int r):Point(x), rad(r){}

Circle(int x, int y, int r):Point(x, y), rad(r){}

Circle(Point p, int r):Point(p), rad(r){}

// методы

void print();

int intersect(Circle c);

};

Теперь - реализация методов

void Circle::print()

{

Point::print();

cout << " with rad " << rad;

}

int Circle::intersect(Circle c)

{

return distance(c) < rad + c.rad;

}

Метод distance из класса Point наследуется классом Circle.

Использование классов:

Circle c1(3), c2(2, 2);

cout << "distance between " << c1 << " and " << c2 << " is " << c1.distance(c2) << endl;

cout << c1 << " and " << c2;

if(c1.intersect(c2))

cout << " are intersected " << endl;

else

cout << " are not intersected " << endl;

Указатели на классы: базовый и производный

Представление экземпляров классов в памяти - производный класс = базовый плюс что-то свое:

class B{ . . . };

class D: public B { . . . };

B obj1; D obj2;

 obj1       obj2

 состояние      часть базового
 класса       класса

         собственная
         часть

 bPtr   dPtr

Объявлени указателей: B *b1Ptr, *b2Ptr; D *d1Ptr, *d2Ptr;

Указатели на тип определяют начальный адрес области памяти и ее размер, поэтому bPtr адресует только состояние, определяемое базовым классом, а dPtr - состояние, определяемое производным классом.

Определение указателей традиционное:

b1Ptr = &obj1; d1Ptr = &obj2; b2Ptr = b1Ptr; d2Ptr = d1Ptr;
b1Ptr = new B; d1Ptr = new D;

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

b1Ptr = &obj2; b2Ptr = d1Ptr; b1Ptr = new D;

Обратное присваивание невозможно; записи вида d1Ptr = b1Ptr; вызовут сообщения об ошибках на этапе компиляции (присваивания указателей разного типа).

Вызов методов по указателю на класс

class B{        class D: public B{

. . .         . . .

public:        public:

. . .         . . .

void f();        void f();

}          }

D obj2, *dPtr = &obj2; B obj1, *b1Ptr = &obj1, *b2Ptr = &obj2;

Имеем следующее представление:

 obj1       obj2

 состояние      часть базового
 класса       класса

         собственная
         часть

 b1Ptr  b2Ptr  dPtr

Выполняем методы:

obj1.f()  Вызывается метод f()базового класса для экземпляра базового класса obj1.

b1Ptr->f()  Вызывается метод f()базового класса по указателю базового класса b1Ptr на экземпляр базового класса obj1.

obj2.f()  Вызывается метод f()производного класса для экземпляра производного класса класса obj2.

dPtr->f()  Вызывается метод f()производного класса по указателю производного класса dPtr на экземпляр производного класса obj2.

b2Ptr->f() Какой метод f()- базового или производного класса - вызывается здесь по указателю b2Ptr  на  экземпляр obj2 производного класса?

Понятие статического и динамического связывания

Связывание - подстановка в коды программы вызовов конкретных функций - методов класса. Имеет смысл только для производных классаов.

Обычно компилятор имеет необходимую информацию для того, чтобы определить, какая функция имеется в виду. Например, если в программе встречается вызов obj.f(), компилятор однозначно выбирает функцию f() в зависимости от типа адресата obj. Если в программе используются указатели на экземпляры класса: ptr->f(), выбор функции - метода класса определяется типом указателя.

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

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

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

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

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

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

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

имеют в прототипе в базовом классе ключевое слово virtual;

обязательно функции-члены класса:

Во всех производных классах должны иметь такой же прототип (указание слова virtual в производных классах не обязательно).

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

Пример: классы Точка и Окружность.

class Point{

protected:

int x, y;

public:

. . .

virtual void print();

};

class Circle: public Point{

private:

int rad;

public:

. . .

void print(); // можно virtual void print();

};

void Point::print()

{

cout << "Point (" << x << ", " << y << ")";

}

void Circle::print()

{

cout << "Circle with center in "; Point::print();

cout << "and radius " << rad;

}

Использование:

Point p1(3,5), p2(1,1), *pPtr;

Cicle c1(1), c2(p2, 1);

pPtr = &p1; pPtr->print(); // получим: Point (3, 5)

pPtr = &c2; pPtr->print(); // получим:

    Circle with center in Point (1, 1) and radius 1

Пример использования динамического связывания: список

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

Рассмотрим пример - список, содержащий и точки, и окружности.

struct Item{

Point *info;

Item *next;

// конструктор

Item():info(NULL), next(NULL){}

Item(Point *p):info(p), next(NULL){}

};

class List{

private:

Item *head;

public:

List():head(NULL){}

void insert(Point *p){p->next = head; head = p;}

void print();

};

void List::print()

{

for(Item *cur = head; cur; cur = cur->next){

 cur->info->print();

 cout << endl;

}

}

Использование класса:

List mylist;

Point *p = new Point(1,2);

mylist.insert(p);

p = new Cicle(1,2,1);

mylist.insert(p);

mylist.print();

получим:

Circle with center in Point (1, 2) and radius 1

Point (1, 2)

Виртуальные деструкторы

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


 

А также другие работы, которые могут Вас заинтересовать

12182. Часть первая: ответы на основные вопросы о процессорах 68.5 KB
  Часть первая: ответы на основные вопросы о процессорах Q: Что такое FPU A: FPU это Floating Point Unit. А проще говоря блок производящий операции с плавающей точкой часто говорят запятой или математический сопроцессор. FPU помогает основному процессору выполнять математические о...
12183. Строении и принцип действия клавиатуры и мыши 98.7 KB
  Лабораторная работа № 18 Строении и принцип действия клавиатуры и мыши 1. Цель работы Изучение принципа действия клавиатуры и мыши. 2. Теоретические сведения Клавиатура Клавиатура выполнена как правило в виде отдельного устройства подключаемого к компьютеру то...
12184. Тестирование ОЗУ 146.7 KB
  Лабораторная работа № 13 Тестирование ОЗУ 1. Цель работы Изучение основных характеристик ОЗУ и выявление их ошибок 2. Теоретические сведения Основными характеристиками ОЗУ являются время доступа быстродействие емкость. Время доступа это промежуток времени за...
12185. Расположение компонентов в ПК 297.4 KB
  Лабораторная работа № 11 Расположение компонентов в ПК. 1. Цель работы Изучение расположения компонентов в ПК и их назначение. 2. Теоретические сведения Компоненты компьютера Если вы пользуетесь настольным компьютером то наверное уже знаете что не существует о
12186. Мониторинг работоспособности материнской платы 41.91 KB
  Лабораторная работа № 17 Мониторинг работоспособности материнской платы 1. Цель работы Научиться диагностировать работоспособность системной платы 2. Теоретические сведения SpeedFan мощная утилита мониторинга Задача мониторинга критически важных параметров р
12187. СИРОВИННІ МАТЕРІАЛИ МАРТЕНІВСЬКОГО ВИРОБНИЦТВА 1.09 MB
  1 СИРОВИННІ МАТЕРІАЛИ МАРТЕНІВСЬКОГО ВИРОБНИЦТВА Шихтові матеріали поділяються на металеві і неметалічні. До металевої частини шихти відносяться: чавун брухт розкислювачі і легуючі добавки; до неметалічної – залізна і марганцева руда окалина агломерат вапняк і ва...
12188. ОСОБЛИВОСТІ ПОБУДОВИ ЗЛИВКІВ СПОКІЙНОЇ, КИПЛЯЧОЇ ТА НАПІВСПОКІЙНОЇ СТАЛЕЙ 797.5 KB
  ОСОБЛИВОСТІ ПОБУДОВИ ЗЛИВКІВ СПОКІЙНОЇ КИПЛЯЧОЇ ТА НАПІВСПОКІЙНОЇ СТАЛЕЙ Особливості побудови зливка спокійної сталі Звичайна структура зливка спокійної сталі рис. 7.1 характеризується наступними основними зонами. Зона 1. Тонкий поверхневий шар що утвор
12189. ВИЗНАЧЕННЯ ВМІСТУ ВУГЛЕЦЮ В СТАЛІ ЗА ДОПОМОГОЮ КАРБОМЕТРУ ALPHA 1.03 MB
  ВИЗНАЧЕННЯ ВМІСТУ ВУГЛЕЦЮ в СТАЛІ ЗА ДОПОМОГОЮ карбометру ALPHA Ціль роботи: вивчити методи контролю вмісту вуглецю в сталі; освоїти один з фізичних методів визначення вуглецю в сталі. Теоретичне введення Перед проведенням лабораторної роботи студент зобовя
12190. ХРОНОМЕТРАЖ ПЛАВКИ В СТАЛЕПЛАВИЛЬНОМУ АГРЕГАТІ 31.5 KB
  ХРОНОМЕТРАЖ ПЛАВКИ В СТАЛЕПЛАВИЛЬНОМУ АГРЕГАТІ Мета роботи: 1. Вивчити конструкцію сталеплавильного агрегату. 2. Ознайомитись з організацією робіт сталеплавильного агрегату. 3. Вивчити технологію плавки в сталеплавильному агрегаті. Перед проведенням ла...