4169

Объективно-ориентированное программирование

Конспект

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

Парадигма программирования. Модульное программирование. Нисходящее программирование. Структурное программирование. Понятия объекта, класса объектов. Основные понятия объектно-ориентированного программирования: инкапсуляция, наследование и полиморфизм.

Русский

2012-11-13

369.5 KB

11 чел.

Введение. Объектно-ориентированное программирование как технология программирования.(2 час.)

Парадигма программирования. Модульное программирование. Нисходящее программирование. Структурное программирование. Понятия объекта, класса объектов. Основные понятия объектно-ориентированного программирования: инкапсуляция, наследование и полиморфизм.

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

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

Рассмотрим наиболее известные из технологий:

- метод "северо-западного" угла (имеется в виду лист бумаги или экран дисплея). Программа пишется сразу от начала до конца, без использования каких-либо общих принципов;

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

- технология объектного программирования: связана с использованием при проектировании программы понятий объектов и их классов.

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

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

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

- размер модуля должен быть ограничен;

- модуль должен выполнять логически целостное и завершенное действие;

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

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

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

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

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

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

- простой последовательности действий;

- конструкции выбора или оператора if;

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

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

- первоначально программа формулируется в виде некоторого неформального действия на естественном языке;

- первоначально определяются входные параметры и результат действия;

- очередной шаг детализации не меняет структуру программы, полученную на предыдущих шагах;

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

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

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

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

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

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

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

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

Понятия объекта, класса объектов. Центральными в ООП являются понятия класса и объекта. Образно говоря, ООП заключается не столько в использовании классов и объектов в программе, сколько в замене принципа программирования "от функции к функции" принципом программирования "от класса к классу".

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

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

Класс - описание множества таких объектов и выполняемых над ними действий.

Это определение можно проиллюстрировать средствами классического Си:

struct myclass

   { int   data1;

   ...

   };

void  method1(struct myclass *this,...)

   { ... this->data1 ... }

void  method2(struct myclass *this,...)

   { ... this->data1 ... }

struct myclass obj1, obj2;

... method1(&obj1,...); ... method2(&obj2,...);

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

- класс определяется как структурированный тип данных (struct);

- объекты определяются как переменные класса;

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

struct matrix

{

// определение структурированного типа matrix и методов,

// реализующих операции matrix * matrix, matrix * double

};

matrix a,b;      // Определение переменных -

double dd;       // объектов класса matrix

a = a * b;       // Использование переопределенных

b = b * dd * 5.0;    // операций

Класс - определенный программистом базовый тип данных.

Объект - переменная класса.

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

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

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

Программирование "от класса к классу" включает в себя ряд новых понятий. Прежде всего, это - инкапсуляция данных, то есть логическое связывание данных с конкретной операцией. Инкапсуляция данных означает, что данные являются не глобальными - доступными всей программе, а локальными - доступными только малой ее части. Инкапсуляция автоматически подразумевает защиту данных. Для этого в структуре class используется спецификатор раздела private, содержащий данные и методы, доступные только для самого класса. Если данные и методы содержатся в разделе public, они доступны извне класса. Раздел protected содержит данные и методы, доступные из класса и любого его производного класса. Наличие последних позволяет говорить об иерархии классов, где есть классы - родители - шаблоны для создания классов - потомков. Объекты, полученные из описания класса, называют экземплярами этого класса.

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

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

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

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

2. Расширение языка С.( 3 час.)

Прототипы функций. Перегрузка функций. Значения формальных параметров по умолчанию. Ссылки и параметры-ссылки. Объявления переменных. Встраиваемые функции. Операции new и delete

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

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

тип_функции имя_функции (спецификация_формальных_параметров);

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

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

Для обеспечения перегрузки функций необходимо для каждого имени определить, сколько разных функций связано с ним, т.е. сколько вариантов сигнатур допустимы при обращении к ним. Предположим, что функция выбора максимального значения элемента из массива должна работать для массивов типа int, long, float, double. В этом случае придется написать четыре разных варианта функции с одним и тем же именем.

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

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

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

тип имя_параметра

тип имя_параметра = умалчиваемое_значение

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

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

type* имя_ссылки инициализатор;

В соответствии с синтаксисом инициализатора, наличие которого обязательно, определение ссылки может быть таким: 

type* имя_ссылки = выражение;

или

type* имя_ссылки (выражение);

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

int L = 777;   //  Определена и инициализирована переменная L

int* RL = L;   //  Значением ссылки RL является адрес переменной L

int* RI(0);    //  Опасная инициализация -  значением ссылки RI 

//  становится адрес объекта, в котором

//  временно размещено нулевое целое значение

В определении ссылки символ «*» не является частью типа, т.е. RL или RI  имеют тип int и именно так должны восприниматься в программе.

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

Функционально ссылка ведет себя подобно обычной переменной, того же, что и ссылка, типа. Для доступа к содержимому участка памяти, на который «смотрит» ссылка, нет необходимости явно выполнять разыменование, как это нужно для указателя. Если рассматривать переменную как пару «имя_переменной - значение_переменной», то инициализированная этой переменной ссылка может быть представлена парой «имя_ссылки - значение_переменной». Из этого становится понятной необходимость инициализации ссылок при их определении. Тут ссылки схожи с константами языка Си++. Раз ссылка есть имя, связанное со значением (объектом), уже размещенным в памяти, то, определяя ссылку, необходимо с помощью начального значения определить тот объект (тот участок памяти), на который указывает ссылка.

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

RL  -=  77;

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

RL = 88;

или

L = 88;

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

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

double  a [] = { 10.0, 20.0, 30.0, 40.0 } ;  //  a - массив

double  *pa = a;         // pa - указатель на массив

double*  ra = a[0] ;    // ra - ссылка на первый элемент массива

double  *  * rpd - a  ;  //  Ссылка на указатель (на имя массива)

Для ссылок и указателей из нашего примера соблюдаются равенства pa == *ra, *pa == ra, rpd == a, ra == a[0].

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

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

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

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

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

Подобно указателю на функцию определяется и ссылка на функцию:

тип_функции (* имя_ссылки) спецификация_параметров)

инициализирующее_выражение;

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

int infunc (float, int);                 // Прототип функции

int  (* iref) (float, int) = infunc;     // Определение ссылки

iref - ссылка на функцию, возвращающую значение типа int и имеющую два параметра с типами float и int. Напомним, что использование имени функции без скобок (и без параметров) воспринимается как адрес функции.

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

Объявления переменных.

3. Классы.(3 час.)

Функции-члены и данные-члены. Интерфейсы и реализация. Конструкторы и инициализация. Конструктор без параметров (по умолчанию). Деструкторы и очистка. Конструктор копирования. Указатель this. Статические члены: функции и данные. Указатели на члены. Структуры и объединения. Константные члены-функции и константные объекты.

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

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

struct date { int month, day, year; };

     // дата:     месяц, день, год  }

 date today;

 void set_date(date*, int, int, int);

 void next_date(date*);

 void print_date(date*);

 // ...

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

struct date {

     int month, day, year;

void set(int, int, int);

 void get(int*, int*, int*);

 void next();

 void print();

 };

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

date today;         // сегодня

 date my_burthday;   // мой день рождения

void f()

 {

     my_burthday.set(30,12,1950);

     today.set(18,1,1985);

     my_burthday.print();

     today.next();

 }

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

void date::next()

 {

     if ( ++day > 28 ) {

         // делает сложную часть работы

     }

 }

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

Интерфейсы и реализация. Описание date в предыдущем примере дает множество функций для работы с date, но не указывает, что эти функции должны быть единственными для доступа к объектам типа date. Это ограничение можно наложить используя вместо struct class:

class date {

     int month, day, year;

 public:

     void set(int, int, int);

     void get(int*, int*, int*);

     void next();

     void print();

 };

Метка public делит тело класса на две части. Имена в первой, закрытой части (private), могут использоваться только функциями членами. Вторая, открытая часть, составляет интерфейс к объекту класса. Обе эти части составляют реализацию объекта. Struct - это просто class, у которого все члены общие, поэтому функции члены определяются и используются точно так же, как в предыдущем случае. Описание date в предыдущем примере дает множество функций для работы с date, но не указывает, что эти функции должны быть единственными для доступа к объектам типа date. Это ограничение можно наложить используя вместо struct class:

class date {

     int month, day, year;

 public:

     void set(int, int, int);

     void get(int*, int*, int*);

     void next();

     void print();

 };

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

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

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

class date {

date(int, int, int);

 };

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

date today = date(23,6,1983);

 date xmas(25,12,0);      // сокращенная форма (xmas - рождество)

 date my_burthday;        // недопустимо, опущена инициализация

Часто бывает хорошо обеспечить несколько способов инициализации объекта класса. Это можно сделать, задав несколько конструкторов. Например:

class date {

     int month, day, year;

 public:

     // ...

     date(int, int, int);    // день месяц год

     date(char*);            // дата в строковом представлении

     date(int);                // день, месяц и год сегодняшние

     date();                    // дата по умолчанию: сегодня

 };

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

date today(4);

 date july4("Июль 4, 1983");

 date guy("5 Ноя");

 date now;                    // инициализируется по умолчанию

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

class date {

     int month, day, year;

 public:

     // ...

     date(int d =0, int m =0, int y =0);

     date(char*);            // дата в строковом представлении

 };

date::date(int d, int m, int y)

 {

     day = d ? d : today.day;

     month = m ? m : today.month;

     year = y ? y : today.year;

     // проверка, что дата допустимая

     // ...

 }

Когда используется значение параметра, указывающее "брать по умолчанию", выбранное значение должно лежать вне множества возможных значений параметра. Для дня day и месяца month ясно, что это так, но для года year выбор нуля неочевиден. К счастью, в европейском календаре нет нулевого года . Сразу после 1 г. до н.э. (year=-1) идет 1 г. н.э. (year=1).

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

date  d = today;    // инициализация посредством присваивания

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

Деструкторы и очистка. Определяемый пользователем тип чаще имеет, чем не имеет, конструктор, который обеспечивает надлежащую инициализацию. Для многих типов также требуется обратное действие, деструктор, чтобы обеспечить соответствующую очистку объектов этого типа. Имя деструктора для класса X есть ~X() ("дополнение конструктора"). В частности, многие типы используют некоторый объем памяти из свободной памяти, который выделяется конструктором и освобождается деструктором. Заметим, что в Си++ для этого используются операторы new и delete. Пример конструктора и деструктора объекта date:

class date { int *day, *month, *year

 public:

 date(int d, int m, int y)

 {

  day=new int;

  month=new int;

  year=new int;

  *day= d ? d : 1;

  *month = m ? m : 1;

  *year = y ? y : 1;

 }

...

~date()

 {

  delete day;

  delete month;

  delete year;

 }

};

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

date date2 = date1;

Однако имеются случаи, в которых создание объекта без вызова конструктора осуществляется неявно:

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

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

Во всех этих случаях транслятор не вызывает конструктора для вновь создаваемого объекта:

- dat2 в приведенном определении;

- создаваемого в стеке формального параметра;

- временного объекта, сохраняющего значение, возвращаемое функцией.

Вместо этого в них копируется содержимое объекта-источника:

- dat1 в приведенном примере;

- фактического параметра;

- объекта - результата в операторе return.

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

class string

       {

       char    *Str;

       int     size;

public:

       string(string&);      // Конструктор копирования

};

string::string(string& right) // Создает копии динамических

       {                     // переменных и ресурсов

       s = new char[right->size];

       strcpy(Str,right->Str);

       }

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

Указатель this. В функции - члене на данные - члены объекта, для которого она была вызвана, можно ссылаться непосредственно. Например:

class x {

     int m;

 public:

     int readm() { return m; }

 };

x aa;

x bb;

 void f()

 {

     int a = aa.readm();

     int b = bb.readm();

     // ...

 }

В первом вызове члена readm() m относится к aa.m, а во втором - к bb.m.

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

x* this;

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

class x {

     int m;

 public:

     int readm() { return this->m; }

 };

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

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

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

имя_класса::имя_элемента

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

#include <stdio.h>

class   dat

       {

       int     day,month,year;

static  dat     *fst;      // Указатель на первый элемент

       dat     *next;     // Указатель на следующий элемент

public:

       void    show();     // Просмотр всех объектов

       dat();              // Конструктор

       ~dat();             // Деструктор

       };

dat     *dat::fst=NULL;     // Определение статического

                          // элемента

void    dat::show()

{

dat    *p;

for (p=fst; p !=NULL; p=p->next)

       { /* вывод информации об объекте */ }

}

//------ Конструктор - включение в начало списка  -------- dat::dat()

{ /* ... */ next = fst;  fst = this; }

//------ Деструктор - поиск и исключение из списка -------

dat::~dat()

{

dat     *&p = fst;              // Ссылка на указатель на

                               // текущий элемент списка

for (; p !=NULL; p = p->next)

   if (p = this)               // Найден - исключить и

       { p = p->next; return;} // и выйти

}

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

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

class   list

       { ...

static  void    show();   // Стaтическая функция просмотра

       }                 // списка объектов

static  void    list::show()

{

list    *p;

for (p=fst; p !=NULL; p=p->next)

       { ...вывод информации об объекте... }

}

void    main()

{ ...

list::show();          // Вызов функции по полному имени

}

Например, для управления задачами в операционной системе или в ее модели часто бывает полезен список всех задач:

class task {

 // ...

 task* next;

 static task* task_chain;

 void shedule(int);

 void wait(event);

 // ...

 };

Описание члена task_chain (цепочка задач) как static обеспечивает, что он будет всего лишь один, а не по одной копии на каждый объект task. Он все равно остается в области видимости класса task, и "извне" доступ к нему можно получить, только если он был описан как public. В этом случае его имя должно уточняться именем его класса:

task::task_chain

В функции члене на него можно ссылаться просто task_chain. Использование статических членов класса может заметно снизить потребность в глобальных переменных.

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

class list

     { ...

static void show(); // Стaтическая функция просмотра списка

     }

static void list::show()

{

list *p;

for (p=fst; p !=NULL; p=p->next)

       { ...вывод информации об объекте... }

}

void    main()

{ ...

list::show();          // Вызов функции по полному имени

}

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

struct cl

 {

 char* val;

 void print(int x) { cout << val << x << "\n"; };

 cl(char* v) { val = v; }

 };

 // ``фальшивый'' тип для функций членов:

 typedef void (*PROC)(void*, int);

 main()

 {

 cl z1("z1 ");

 cl z2("z2 ");

 PROC pf1 = PROC(&z1.print);

 PROC pf2 = PROC(&z2.print);

 z1.print(1);

 (*pf1)(&z1,2);

 z2.print(3);

 (*pf2)(&z2,4);

 }

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

C++ поддерживает понятие указатель на член: cl::* означает "указатель на член класса cl". Например:

typedef void (cl::*PROC)(int);

 PROC pf1 = &cl::print;  // приведение к типу ненужно

 PROC pf2 = &cl::print;

Для вызовов через указатель на функцию - член используются операции . и ->. Например:

(z1.*pf1)(2);

 ((&z2)->*pf2)(4);)

Структуры и объединения. По определению struct - это просто класс, все члены которого общие, то есть

struct s { ...

есть просто сокращенная запись

class s { public: ...

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

Именованное объединение определяется как struct, в которой все члены имеют один и тот же адрес. Если известно, что в каждый момент времени нужно только одно значение из структуры, то объединение может сэкономить пространство. Например, можно определить объединение для хранения лексических символов Cи - компилятора:

union tok_val {

 char* p;    // строка

 char v[8];    // идентификатор (максимум 8 char)

 long i;     // целые значения

 double d;   // значения с плавающей точкой

 };

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

void strange(int i)

 {

 tok_val x;

 if (i)

   x.p = "2";

 else

   x.d = 2;

 sqrt(x.d);    // ошибка если i != 0

 }

Кроме того, объединение, определенное так, как это, нельзя инициализировать. Например:

tok_val curr_val = 12;  // ошибка: int присваивается tok_val'у

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

union tok_val {

 char* p;    // строка

 char v[8];    // идентификатор (максимум 8 char)

 long i;     // целые значения

 double d;   // значения с плавающей точкой

tok_val(char*);   // должна выбрать между p и v

 tok_val(int ii)   { i = ii; }

 tok_val()     { d = dd; }

 };

Это позволяет справляться с теми ситуациями, когда типы членов могут быть разрешены по правилам для перегрузки имени. Например:

void f()

 {

 tok_val a = 10;  // a.i = 10

 tok_val b = 10.0;  // b.d = 10.0

 }

Когда это невозможно (для таких типов, как char* и char[8], int и char, и т.п.), нужный член может быть найден только посредством анализа инициализатора в ходе выполнения или с помощью задания дополнительного параметра. Например:

tok_val::tok_val(char* pp)

 {

 if (strlen(pp) <= 8)

   strncpy(v,pp,8);  // короткая строка

 else

   p = pp;     // длинная строка

 }

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

class tok_val {

 char tag;

 union {

 char* p;

 char v[8];

 long i;

 double d;

 };

 int check(char t, char* s)

   { if (tag!=t) { error(s); return 0; } return 1; }

 public:

 tok_val(char* pp);

 tok_val(long ii) { i=ii; tag='I'; }

 tok_val(double dd) { d=dd; tag='D'; }

long& ival()   { check('I',"ival"); return i; }

 double& fval()   { check('D',"fval"); return d; }

 char*& sval()  { check('S',"sval"); return p; }

 char*  id()    { check('N',"id"); return v; }

 };

Конструктор, получающий строковый параметр, использует для копирования коротких строк strncpy(). strncpy() похожа на strcpy(), но получает третий параметр, который указывает, сколько символов должно копироваться:

tok_val::tok_val(char* pp)

 {

 if (strlen(pp) <= 8) {   // короткая строка

   tag = 'N'

   strncpy(v,pp,8);   // скопировать 8 символов

 }

 else {         // длинная строка

   tag = 'S'

   p = pp;      // просто сохранить указатель

 }

 }

Тип tok_val можно использовать так:

void f()

 {

 tok_val t1("short");    // короткая, присвоить v

 tok_val t2("long string");  // длинная строка, присвоить p

 char s[8];

 strncpy(s,t1.id(),8);   // ok

 strncpy(s,t2.id(),8);   // проверка check() не пройдет

 }

Константные члены-функции и константные объекты. Иногда требуются исключения из правил доступа, когда некоторой функции или классу требуется разрешить доступ к личной части объекта класса. Это согласуется с тем принципом, что сам класс определяет права доступа к своим объектам "со стороны". К средствам контроля доступа относятся объявления функций-членов константными (const). В этом случае они не имеют права изменять значение текущего объекта, с которым вызываются. Заголовок такой функции при этом имеет вид

void dat::put() const

    { ... }

Аналогично можно определить константные объекты:

const class a{...} value;

4. Дружественные функции и перегрузка операций.(5 час.)

Функции-друзья. Перегрузка бинарных и унарных операций. Перегруженные операции индексирования, вызова функций, инкремента и декремента префиксных и постфиксных. Перегрузка new, delete. Преобразование типов, определяемых пользователем с помощью конструкторов и операций преобразования. Неявное преобразование типов. Друзья-функции и друзья-классы.

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

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

class   A{

       int     x;   // Личная часть класса

       ...

friend  class   B;   // Функции класса B дружественны A

                    // (имеют доступ к приватной части A)

friend  void  C::fun(A&);// Элемент-функция fun класса C имеет

                        // доступ к приватной части A

friend  void    xxx(A&,int);// Функция xxx дружественна классу A

friend  void    C::operator+(А&);

                    // Переопределяемая в классе C операция

       };           // <объект C>+<объект A> дружественна

                    // классу A

class   B            // Необходим доступ к личной части A

       {

public: int     fun1(A&);

       void    fun2(A&);

       };

class   C

       {

public: void    fun(A&);

       void    operator+(A&);

       };

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

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

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

+  -  *  /  %  ^  &  |  ~  !

=  <  >  += -= *= /= %= ^= &=

|= << >> >>= <<= == != <= >= &&

|| ++ -- [] () new delete

Последние четыре - это индексирование, вызов функции, выделение свободной памяти и освобождение свободной памяти. Изменить приоритеты перечисленных операций невозможно, как невозможно изменить и синтаксис выражений. Нельзя, например, определить унарную операцию % или бинарную !. Невозможно определить новые лексические символы операций, но в тех случаях, когда множество операций недостаточно, вы можете использовать запись вызова функции. Используйте например, не **, а pow(). Эти ограничения могут показаться драконовскими, но более гибкие правила могут очень легко привести к неоднозначностям. Например, на первый взгляд определение операции **, означающей возведение в степень, может показаться очевидной и простой задачей, но подумайте еще раз. Должна ли ** связываться влево (как в Фортране) или вправо (как в Алголе)? Выражение a**p должно интерпретироваться как a*(*p) или как (a)**(p)? Имя функции операции есть ключевое слово operator (то есть, операция), за которым следует сама операция, например, operator<<. Функция операция описывается и может вызываться так же, как любая другая функция. Использование операции - это лишь сокращенная запись явного вызова функции операции. Например:

void f(complex a, complex b)

{

  complex c = a + b;     // сокращенная запись

  complex d = operator+(a,b); // явный вызов

 }

Для переопределения операции используется особая форма функции-элемента с заголовком такого вида:

operator операция( список_параметров-операндов)

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

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

если функция задается как обычная функция-элемент класса, то первым операндом операции является объект класса, указатель на который передается неявным параметром this;

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

Естественно, что полное имя такой функции не содержит имени класса.

Бинарная операция может быть определена или как функция член, получающая один параметр, или как функция друг, получающая два параметра. Таким образом, для любой бинарной операции @ aa@bb может интерпретироваться или как aa.operator@(bb), или как operator@(aa,bb). Если определены обе, то aa@bb является ошибкой. Унарная операция, префиксная или постфиксная, может быть определена или как функция член, не получающая параметров, или как функция друг, получающая один параметр. Таким образом, для любой унарной операции @ aa@ или @aa может интерпретироваться или как aa.operator@(), или как operator@(aa). Если определена и то, и другое, то и aa@ и @aa являются ошибками. Рассмотрим следующие примеры:

class X {

// друзья

friend X operator-(X);   // унарный минус

  friend X operator-(X,X);  // бинарный минус

  friend X operator-();    // ошибка: нет операндов

  friend X operator-(X,X,X); // ошибка: тернарная

// члены (с неявным первым параметром: this)

  X* operator&(); // унарное & (взятие адреса)

  X operator&(X); // бинарное & (операция И)

  X operator&(X,X);  // ошибка: тернарное

};

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

В качестве примера рассмотрим доопределение стандартных операций над датами:

static  int days[]={ 0,31,28,31,30,31,30,31,31,30,31,30,31};

class   dat {

       int  day,month,year;

public:

  void next();       // Элемент-функция вычисления следующего дня

  dat operator++();  // Операция ++

  dat operator+(int);// Операция "дата + целое" с передачей

                     // первого операнда через this

friend dat operator+(int,dat);// Операция с явной передачей всех

                             //аргументов по значению

            dat(int=0,int=0,int=0);

            dat(char *);        //

            ~dat();             //

       };                       //

//------ Функция вычисления следующего дня -----------------

//      Используется ссылка на текущий объект this,

//      который изменяется в процессе операции

void dat::next()

{

day++;

if (day > days[month])

       {

       if ((month==2) && (day==29) && (year%4==0)) return;

       day=1;  month++;

       if (month==13)

               {

               month=1; year++;

               }

       }

}

//------ Операция инкремента даты -------------------------

// 1. Первый операнд по указателю this

// 2. Возвращает копию входного объекта (операнда)

//    до увеличения

// 3. Соответствует операции dat++ (увеличение после

//    использования)

// 4. Замечание: для унарных операций типа -- или ++

//    использование их до или после операнда не имеет

//    значения (вызывается одна и та же функция).

dat     dat::operator++()

{                // Создается временный объект

dat x = *this;   // В него копируется текущий объект

dat::next();     // Увеличивается значение текущего объекта

return(x);       // Возвращается временный объект по

}                // значению

//------ Операция "дата + целое" --------------------------

//  1. Первый операнд по указателю this

//  2. Входной объект не меняется, результат возвращается

//     в виде значения автоматического объекта x

dat     dat::operator+(int n)

{

dat     x;

x = *this;               // Копирование текущего объекта в x

while (n-- !=0) x.next();// Вызов функции next для объекта x

return(x);               // Возврат объекта x по значению

}

//------ Операция "целое + дата" -------------------------

// 1. Дружественная функция с полным списком операндов

// 2. Второй операнд класса dat - передается по значению,

// поэтому может модифицироваться без изменения исходного

// объекта

dat     operator+(int n, dat p)

{

while (n-- !=0) p.next();  // Вызов функции next для p

return(p);                 // Возврат копии объекта p

}

void main()

{

int i;

dat a, b(17,12,1990), c(12,7), d(3), e;

dat *p = new dat[10];

e = a++;

d = b+15;

for (i=0; i<10; i++) p[i] = p[i] + i;

delete[10] p;

}

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

//------ Операция "дата + целое" --------------------------

// 1. Функция с неявным первым операндом по указателю this

// 2. Меняется значение текущего объекта

// 3. Результат - ссылка на текущий объект

dat&  dat::operator+(int n)

{

while (n-- !=0) next();   // Вызов next с текущим объектом

return(*this);            // Возврат ссылки на объект this

}

//------ Операция "дата + целое" -------------------------

// 1. Дружественная функция с полным списком аргументов

// 2. Первый операнд класса dat - ссылка, меняется при

//    выполнении операции

// 3. Результат - ссылка на операнд

dat&    operator+(dat& p,int n)

{

while (n-- !=0) p.next();    // Вызов next для объекта p,

                            // заданного ссылкой

return(p);                   // Возврат ссылки на p

}

//----- Операция "целое + дата" --------------------------

// 1. Дружественная функция с полным списком аргументов

// 2. Второй операнд класса dat - ссылка, меняется при

//    выполнении операции

// 3. Результат - ссылка на операнд

//--------------------------------------------------------

dat&    operator+(int n, dat& p)

{

while (n-- !=0) p.next(); // Вызов next для объекта p,

                         // заданного ссылкой

return(p);                // Возврат ссылки на p

}

void    main()

{

dat a,b;                // "Арифметические" эквиваленты

a + 2 + 3;              // a = a + 2; a = a + 3;

5 + b + 4;              // b = 5 + b; b = b + 4;

}

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

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

//------ Операция "дата + целое" --------------------------

// 1. Функция с неявным первым операндом по указателю this

// 2. Изменяется автоматический объект - копия операнда

// 3. Результат - значение автоматического объекта

dat   dat::operator+(int n)

{

dat   tmp = *this;       // Объект - копия операнда

while (n-- !=0) tmp.next();// Вызов next с объектом tmp

return(tmp);               // Возврат значения объекта tmp

}

//------ Операция "дата + целое" -------------------------

// 1. Дружественная функция с полным списком аргументов

// 2. Первый параметр класса dat передается по значению,

//    является копией первого операнда и меняется при

//    выполнении операции

// 3. Результат - значение формального параметра

dat    operator+(dat p,int n)

{

while (n-- !=0) p.next();    // Вызов next для объекта p,

                            // копии операнда

return(p);                   // Возврат значения

}                            // формального параметра

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

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

class string

       {

       char    *Str;

       int     size;

public:

string  &operator =(string&);

       };

string  &string::operator=(string& right)

{

if (Str !=NULL) delete Str;// Освободить динамическую память

size = Str.right.size;          // Резервировать память

Str = new char[size];

strcpy(Str,right->Str);         // Копировать строки

}

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

//------Переопределение операций [] и ()

#include <string.h>

class   string          // Строка переменной длины

       {

       char    *Str;   // Динамический массив символов

       int     size;   // Длина строки

public:

string  operator()(int,int); // Операция выделения подстроки

char    operator[](int);     // Операция выделения символа

int     operator[](char*);   // Операция поиска подстроки

       };

//------ Операция выделения подстроки -------------------

string  string::operator()(int n1, int n2) {

string  tmp = *this;

delete  tmp.Str;

tmp.Str = new char[n2-n1+1];

strncpy(tmp.Str, Str+n1, n2-n1); }

Пример переопределения операции инкремента приведен выше. Переопределение декремента производится аналогично. Заметим только, что когда операции ++ и -- перегружены, префиксное использование и постфиксное в классическом С++ различить невозможно. В современной версии языка (Microsoft Visual C++ 6.0) принято соглашение, что перегрузка префиксных операций ++ и -- ничем не отличаются от перегрузки других унарных операций, то есть дружественные функции operator++() и operator--() с одним параметром некоторого класса определяют префиксные операции ++ и --. Операции - члены класса без параметров определяют те же префиксные операции. При расширении действия постфиксных операций ++ и операции-функции должны иметь еще один дополнительный параметр типа int. Если для перегрузки используется операция - член класса, то она должна иметь один параметр типа int. Если операция определена как дружественная функция, то ее первый параметр должен иметь тип класса, а второй - тип int. Когда в программе используется соответствующее постфиксное выражение, то операция - функция вызывается с нулевым целым параметром.

Рассмотрим пример применения разных операций - функций для постфиксной и префиксной операций ++ и --.

class pair   // класс «пара чисел»

{   int N;  // целое

  double x; // вещественное

friend pair& operator ++ (pair&); //дружественная для префикса

friend pair& operator ++(pair&,int);//дружественная для постфикса

public:

pair (int n, double xn) //конструктор

{ N = n; x = xn; }

void display ()  //вывод значения

{ printf (”N = % d x = % f\n”, N, x); }

pair & operator –- () //член для префикса

{ N /= 10; x /= 10; return *this; }

pair & operator –- ( int k) //член для постфикса

{ N /= 2; x /= 2; return *this; }

};

pair & operator ++ ( pair & P) // дружественная для префикса

{ P.N *= 10; P.x *= 10; return P; }

pair & operator ++ (pair & P, int k)// дружественная для постфикса

{ P.N = P.N * 2 + k; P.x = P.x * 2 + k; return P; }

void main ()

{ pair Z (10, 20.0); //вызов конструктора

 Z.display();   //N = 10 x = 20

++Z; Z.display(); //N = 100 x = 200

--Z; Z.display(); //N = 10 x = 20

Z++; Z.display(); //N = 20 x = 40

Z--; Z.display(); //N = 10 x = 20

}

Для демонстрации полной независимости смысла перегруженной операции от ее традиционного значения  в операциях - функциях для префиксных операций ++ соответствует увеличению в 10 раз, а –- уменьшению в 10 раз. Для постфиксных операций ++ определена как увеличение в 2 раза, а -- уменьшение в 2 раза. Попытки использовать в постфиксных операциях значение дополнительного параметра int k подтверждают его равенство 0.

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

void *operator new(size_t size);

void  operator delete (void *);

где void * - указатель на область памяти, выделяемую под объект, size - размер объекта в байтах, size_t - тип размерности области памяти, int или long.

Переопределение этих операций позволяет написать собственное распределение памяти для объектов класса.

Операции, не допускающие перегрузки. В С++ существует несколько операций, не допускающих перегрузки:

. прямой выбор члена объекта класса;

.* обращение к члену через указатель на него;

?: условная операция;

:: операция указания области видимости;

sizeof операция вычисления размера в байтах;

# препроцессорная операция.

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

Сами преобразования типов происходят в тех же самых случаях, что и обычные преобразования базовых типов данных:

при использовании операции явного преобразования типов;

при выполнении операции присваивания, если она не переопределена в виде "xxx=yyy" (транслятором создается временный объект класса "xxx", для которого вызывается указанный конструктор и который затем используется в правой части операции присваивания);

при неявном преобразовании типа формального параметра функции при передаче его по значению (вместо конструктора копирования);

при неявном преобразовании типа результата функции при передаче его по значению (вместо конструктора копирования);

при определении объекта класса "xxx" одновременно с его инициализацией объектом класса "yyy" (вместо конструктора копирования)

yyy     b;

xxx     a = b;

При конструировании объекта класса "xxx" с использованием объекта класса "yyy" естественно должна быть обеспечена доступность необходимых данных последнего (например, через дружественность).

В качестве примера рассмотрим обратное преобразование базового типа long к типу dat - количество дней от начала летоисчисления преобразуется к дате. Здесь же рассмотрим другой класс - man, в котором одним из элементов личной части является дата. Значение этого объекта копируется при преобразовании типа man в тип dat.

static  int days[]={0,31,28,31,30,31,30,31,31,30,31,30,31};

class   man;

class   dat

       {

       int     day,month,year;

public:

       dat(long);            // Преобразование long в dat

       dat(man&);            // Преобразование man  в dat

       dat() {}

       };

class   man

       {

       friend  class dat;

       dat     WasBorn; // объект класса dat в объекте класса man

public:

       man(dat&);            // Преобразование dat в man

};

dat::dat(man& p)

{  *this = p.WasBorn; }

man::man(dat& p)

{  WasBorn = p; }

dat::dat(long p)

{

year = p / 365.25;         // Число лет с учетом високосных

p-=(year-1)*365L - year/4; // Остаток дней в текущем году

year++;                    // Начальный год - 0001

for (month=1; p > 0; month++)

       {                  // Вычитание дней по месяцам

       p -= days[month];

       if (month == 2 && year % 4 == 0) p--;

       }

month--;                        // Восстановление последнего

p += days[month];               // месяца

if (month == 2 && year % 4 == 0) p++;

day = p + 1;

}

Преобразование объектов класса имеет место при выполнении операций явного приведения типов, присваивания,  а также при определении объектов с инициализацией их объектами приводимого класса

long l=1000;

dat  a = l, b;          // Вызов конструктора dat(long)

man  c = a;             // Вызов конструктора man(dat&)

man f(man a)

{ return(a); }

void main()

{

a = 2000L;              // Вызов конструктора dat(long)

(dat)3000L;             // Вызов конструктора dat(long)

c = b;                  // Вызов конструктора man(dat&)

b = f(b);               // Вызов конструктора dat(man&)

}

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

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

#include <stdio.h>

static  int days[]={0,31,28,31,30,31,30,31,31,30,31,30,31};

class   dat

       {

       int     day,month,year;

public:

       operator int();        // Преобразование dat в int

       operator long();       // Преобразование dat в long

long    operator -(dat &p);    // Операция dat-dat вычисляет

       dat();                 // разность дат в днях

       dat(char *);

};

dat::operator int()

{

int     r;                         // Текущий результат

int     i;                         // Счетчик месяцев

for (r=0, i=1; i<month; i++)       // Число дней в прошедших

       r += days[month];          // месяцах

if ((month>2) && (year%4==0)) r++; // Високосный год

r += day;                          // Дней в текущем месяце

return(r);

}

dat::operator long()

{

long    r;                 // Текущий результат

r = 365 * (year-1);        // Дней в предыдущих полных годах

r += year / 4;             // Високосные года

r += (int)(*this);         // Дней в текущем году

return(r);

}

long    dat::operator-(dat& p)

{return((long)(*this) - (long)p);}

void main()

{

dat     a("12-05-1990");    // Дата, заданная строкой

dat     b;                  // Текущая дата

int     c;

long    d;

printf("С 12-05-1990 прошло %4ld дней\n",(long)b-(long)a);

printf("В этом году прошло %3d дней\n",(int)b);

c = b;

d = b - a;              // Операция dat-dat

printf("С 12-05-1990 прошло %4ld дней\n",d);

printf("В этом году прошло %3d дней\n",c);

}

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

class X {

  // ...

  X(int);

  int m();

  friend int f(X&);

};

Внешне не видно никаких причин делать f(X&) другом дополнительно к члену X::m() (или наоборот), чтобы реализовать действия над классом X. Однако член X::m() можно вызывать только для "настоящего объекта", в то время как друг f() может вызываться для объекта, созданного с помощью неявного преобразования типа. Например:

void g()

{

  1.m();   // ошибка

  f(1);    // f(x(1));

}

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

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

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

5. Производные классы.(3 час.)

Наследование классов и производные классы. Конструкторы, деструкторы и наследование. Множественное наследование. Виртуальные базовые классы. Иерархия классов. Виртуальные функции. Полиморфизм. Абстрактные классы и чистые виртуальные функции.

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

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

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

Иерархия классов позволяет определять новые классы на основе уже имеющихся. Имеющиеся классы обычно называют базовыми (иногда порождающими), а новые классы, формируемые на основе базовых, - производными (порожденными), иногда классами-потомками или наследниками. Производные классы «получают наследство» - данные и методы своих классов - и, кроме того, могут пополняться собственными компонентами (данными и собственными методами). Наследуемые компоненты не перемещаются в производный класс, а остаются в базовых классах. Сообщение, обработку которого не могут выполнить методы производного класса, автоматически передается в базовый класс. Если для обработки сообщения нужны данные, отсутствующие в производном классе, то их пытаются отыскать автоматически и незаметно для программиста в базовом классе.

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

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

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

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

В иерархии классов соглашение относительно доступности компонентов класса следующее.

Собственные (private) методы и данные доступны только внутри того класса, где они определены.

Защищенные (protected) компоненты доступны внутри класса, в котором они определены, и дополнительно доступны во всех производных классах.

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

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

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

Определение производного класса. В определении и описании производного класса приводится список базовых классов, из которых он непосредственно наследует данные и методы. Между именем вводимого (нового) класса и списком базовых классов помещается двоеточие. Например, при таком определении

class S: X, Y, Z { ...};

класс S порожден классами X, Y, Z, откуда он наследует компоненты. Наследование компонента не выполняется, если его имя будет использовано в качестве имени компонента в определении производного класса S. Как уже говорилось, по умолчанию из базовых классов наследуются методы и данные со спецификаторами доступа - public (общедоступные) и protected (защищенные).

В порожденном классе эти унаследованные компоненты получают статус доступа private, если новый класс определен с помощью ключевого слова class, и статус доступа public, если новый класс определен как структура, т.е. с помощью ключевого слова struct. Таким образом при определении класса  struct J: X, Z { ... }; любые наследуемые компоненты классов X, Z будут иметь в классе J статус общедоступных (public). Пример:

class B { protected: int t;

public: char u;

};

class E: B { ... };  // t, и наследуются как private

class S: B { ... }; // t, и наследуются как public

Явно изменить умалчиваемый статус доступа при наследовании можно с помощью спецификаторов доступа - private, protected и public. Эти спецификаторы доступа указываются в описании производного класса непосредственно перед нужными именами базовых классов. Если класс B определен так, как показано выше, то можно ввести следующие производные классы:

class M: protected B { ... }; // t, и наследуется как protected

class P: public B { ... }; // t - protected, и - public

class D: private B { ... }; // t, и наследуется как  private

struct F: private B { ... }; // t, и наследуется как private

struct G: public B { ... }; t - protected, и - public

Конструктор базового класса всегда вызывается и выполняется до конструктора производного класса.

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

имя_класса

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

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

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

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

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

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

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

class X1 { ... };

class X2 { ... };

class X3 { ... };

class Y1: public X1, public X2, public X3 { ... };

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

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

class X { ...; f () ;  ... };

class Y: public X { ... };

class Z: public X { ... };

class D: public Y, public Z { ... };

В данном примере класс Х дважды опосредовано наследуется классом D.

Проиллюстрированное дублирование класса соответствует включению в производный объект нескольких объектов базового класса. В нашем примере существуют два объекта класса Х, и поэтому для устранения возможных неоднозначностей вне объектов класса D нужно обращаться к конкретному компоненту класса Х, используя полную квалификацию: D::Y::X::f() или D::Z::X::f(). Внутри объекта класса D обращения упрощаются Y::X::f() или Z::X::f(), но тоже содержат квалификацию.

Чтобы устранить дублирование объектов непрямого базового класса при множественном наследовании, этот базовый класс объявляют виртуальным. Для этого в списке базовых классов перед именем класса необходимо поместить ключевое слово virtual. Например, класс Х будет виртуальным базовым классом при таком описании:

class X { ... f() ; ... };

class Y: virtual public X { ... };

class Z: virtual public X { ... };

class D: public Y,  public Z { ... };

Теперь класс D будет включать только один экземпляр Х, доступ к которому равноправно имеют классы Y и Z.

Обратите внимание, что размеры производных классов при отсутствии виртуальных базовых равны сумме длин их компонентов и длин унаследованных базовых классов. «Накладные расходы» памяти здесь отсутствуют.

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

class X { ... };

class Y: virtual public X { ... };

class Z: virtual public X { ... };

class B: virtual public X { ... };

class C: virtual public X { ... };

class E: public X { ... };

class D: public X { ... };

class A: public D, public B,  public Y,  public Z,  public C,  public E { ... };

В данном примере объект класса А включает три экземпляра объектов класса Х: один виртуальный, совместно используемый классами B, Y, C, Z, и два не виртуальных относящихся соответственно к классам D и E. Таким образом, можно констатировать, что виртуальность класса в иерархии производных классов является не свойством класса как такового, а результатом особенностей процедуры наследования.

Возможны и другие комбинации виртуальных и невиртуальных  базовых классов. Например:

class BB { ... };

class AA: virtual public BB { ... };

class CC: virtual public BB { ... };

class DD: public AA,  public CC,  public virtual BB { ... };

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

class X { public: int d; ... };

class Y { public: int d; ... };

class Z:  public X, public Y,

{ public:

int d;

...

d=X::d + Y::d;

...

};

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

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

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

Рассмотрим теперь, как ведут себя при наследовании невиртуальные компонентные функции с одинаковыми именами, типами и сигнатурами параметров.

Если в базовом классе определена некоторая компонентная функция, то такая же функция (с тем же именем, того же типа и с тем же набором и типами параметров) может быть введена в производном классе. Рассмотрим следующее определение классов:

// BASE.DIR - определения базового и производного классов

class base { public:

void fun (int i)

{ printf("\nbase::i =",i); }

};

class dir: public base

{ public: void fun (int i)

{ printf("\nbase::i =",i); }

};

В данном случае внешне одинаковые функции void fun (int) определены в базовом классе base и в производном классе dir.

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

В программе, где определены и доступы оба класса base и dir, обращения к функциям  fun() могут быть выполнены с помощью указателей на объекты соответствующих классов:

// одинаковые функции в базовом и производном классах

# inclube <stdio.h>

# inclube «base.dir» // Определения классов

void main (void)

{ base B, *bp = &B;

dir D, *dp = &D;

base *pbd = &D;

bp->fun (1);          // Печатает : base::i = 1

dp->fun (5);         // Печатает : dir::i = 5

pbd->fun (4);      // Печатает : base::i = 4

}

В программе введены три указателя на объекты разных классов. Следует обратить внимание на инициализацию указателя pbd. В ней адрес объекта производного класса (объекта D) присваивается указателю на объект его прямого базового класса (base *). При этом выполняется стандартное преобразование указателей, предусмотренное синтаксисом языка Си++. Обратное образование, т.е. преобразование указателя на объект базового класса в указатель на объект производного класса, невозможно (запрещено синтаксисом). Обращения к функциям классов base и dir с помощью указателей bp и dp не представляют особого интереса. Вызов pbd->fun() требуется прокомментировать. Указатель pbd имеет тип base*, однако его значение - адрес объекта D класса dir.

Какая же из функций base::fun() или dir::fun() вызывает при обращении pbd->fun()? Результат выполнения программы показывает, что вызывается функция из базового класса. Именно такой вызов предусмотрен синтаксисом языка Си++, т.е. выбор функции (не виртуальной) зависит только от типа указателя, но не от его значения. «Настроив» указатель базового класса на объект производного класса, не удается с помощью этого указателя вызвать функцию из производного класса.

Пусть в этом классе определена компонентная функция void show(). Доступ к функции show() производного класса возможен только с помощью явного указания области видимости:

имя_производного_класса:: show()

либо с использованием имени конкретного объекта:

имя_объекта_производного_класса. show()

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

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

// виртуальные функции в базовом и производном классах

# inclube <stdio.h>

class base { public: virtual void vfun (int i)

{ printf("\nbase::i =",i); }

};

class dir1: public base { public:  void vfun (int i)

{ printf("\ndir1::i =",i); }

};

class dir2: public base { public:  void vfun (int i)

{ printf("\nbase::i =",i); }

};

void main (void)

{ base B, *bp = &B;

dir1 D1, *dp1 = &D1;

dir2 D2, *dp2 = &D2;

base *pbd = &D;

bp->vfun (1);          // Печатает : base::i = 1

dp1->vfun (2);         // Печатает : dir1::i = 2

dp1->vfun (3);         // Печатает : dir2::i = 3

bp =&D1; bp->vfun (4); // Печатает : dir1::i = 4

bp =&D2; bp->vfun (5); // Печатает : dir1::i = 5

}

Заметим, что доступ к функциям vfun() организован через указатель bp на базовый класс. Когда он принимает значение адреса объекта базового класса, то вызывается функция из базового класса. Когда указателю присваиваются значения ссылок на объекты производных классов &D1, &D2, выбор соответствующего экземпляра функции определяется именно объектом. Таким образом, интерпретация каждого вызова виртуальной функции через указатель на базовый класс зависит от значения этого указателя, то есть от типа объекта, для которого выполняется вызов. Для невиртуальной функции ее вызов через указатель интерпретируется взависимости от типа указателя. 

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

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

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

// особенности виртуальных функций

# inclube <stdio.h>

class base { public:

virtual void f1 (void){ printf("\nbase::f1"); }

virtual void f2 (void){ printf("\nbase::f2"); }

virtual void f3 (void){ printf("\nbase::f3"); }

};

class dir: public base { public:

   void f1 (void){ printf("\ndir::f1"); } // виртуальная

   //int f2 (void){ printf("\ndir::f2"); } // ошибка в типе

   void f3(int i){printf("\ndir::f3:%d",i);} //невиртуальная

};

void main (void)

{ base B, *bp = &B; dir D, *dp = &D;

bp->f1();          // Печатает : base::f1

bp->f2();          // Печатает : base::f2

bp->f3();          // Печатает : base::f3

dp->f1();          // Печатает : dir::f1

dp->f2();          // Печатает : base::f2

//dp->f3();          // Не печатает - вызов без параметра

dp->f3(3);  // Печатает : dir::f3::3

dp = &D;

bp->f1();          // Печатает : dir::f1

bp->f2();          // Печатает : base::f2

bp->f3();          // Печатает : base::f3

bp->f3(3);          // Не печатает - лишний параметр

}

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

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

class base { public:

virtual int f (int j) { return j * j; }

};

class dir: public base { public:

int f (int i){return base::f (i * 2); }

};

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

virtual тип имя_функции (список_формальных_параметров) = 0;

В этой записи конструкция «= 0» называется «чистый спецификатор». Пример описания чистой виртуальной функции:

virtual void fpure (void) = 0;

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

class B { protected:

virtual void f (int) = 0;

void s(int);

};

class D: public B {

 . . .

void f (int);

};

class E: public B { void s (int);

};

Здесь B - абстрактный, D - нет, поскольку f - переопределена, а s - наследуется, E - абстрактный, так как s - переопределена, а f - наследуется.

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

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

6. Классы потоков С++.(4 час.)

Заголовочные файлы. Предопределенные объекты и потоки. Операции помещения и извлечения. Форматирование. Флаги форматирования. Манипуляторы. Ошибки потоков. Файловый ввод-вывод с применением потоков С++. Конструкторы файловых потоков. Открытие файлов в разных режимах. Ввод-вывод в файлы.

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

определение используемых типов данных в формальных параметрах и результатах функций с использованием оператора typedef;

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

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

#include <alloc.h>      - заголовочный файл из системного каталога

#include "myhead.h"     - заголовочный файл из текущего (явно указанного) каталога

Процесс подготовки библиотеки включает в себя следующие шаги

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

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

включение объектного модуля в библиотеку.

Предопределенные объекты и потоки. В стандартной библиотеке ввода/вывода стандартного Си (заголовочный файл библиотеки - <stdio.h>) имеются внешние переменные-указатели на дескрипторы файлов - стандартных устройств ввода-вывода.

extern  FILE    *stdin, *stdout, *stderr, *stdaux, *stdprn;

стандартный ввод

стандартный вывод

регистрация ошибок

дополнительное устройство

устройство печати

Эти файлы открываются библиотекой автоматически перед выполнением функции main и по умолчанию назначаются на терминал (stdin - клавиатура, stdout, stderr - экран), последовательный порт (stdaux) и принтер (stdprn). stdin и stdout могут быть переназначены в командой строке запуска программы на любые другие файлы

>test.exe  <a.dat >c:\xxx\b.dat

 файл stdout

 файл stdin

В Си++ существуют классы потоков ввода-вывода, которые являются объектно-ориентированным эквивалентом (stream.h) стандартной библиотеки ввода-вывода (stdio.h):

ios              базовый потоковый класс

streambuf        буферизация потоков

istream          потоки ввода

ostream          потоки вывода

iostream         двунаправленные потоки

iostream_withassign  поток с переопределенной операцией присваивания

istrstream       строковые потоки ввода

ostrstream       строковые потоки вывода

strstream        двунаправленные строковые потоки

ifstream         файловые потоки ввода

ofstream         файловые потоки вывода

fstream          двунаправленные файловые потоки

Стандартные потоки (istream,ostream,iostream) служат для работы с терминалом.

Строковые потоки (istrstream, ostrstream, strstream) служат для ввода-вывода из строковых буферов, размещенных в памяти.

Файловые потоки (ifstream, ofstream, fstream) служат для работы с файлами.

Следующие объекты-потоки заранее определены и открыты в программе перед вызовом функции main:

extern istream cin;  // Стандартный поток ввода с клавиатуры

extern ostream cout; // Стандартный поток вывода на экран

extern ostream cerr; // Стандартный поток вывода сообщений об ошибках (экран)

extern ostream cerr;// Стандартный буферизованный поток вывода сообщений об ошибках (экран).

Операции помещения и извлечения. Для начала рассмотрим пример:

#include <stream.h>

main()

{

    cout << "Hello, world\n";

 }

Строка #include <stream.h> сообщает компилятору, чтобы он включил стандартные возможности потока ввода и вывода, находящиеся в файле stream.h. Без этих описаний выражение cout << "Hello, world\n" не имело бы смысла. Операция << ("поместить в") пишет свой первый аргумент во второй (в данном случае, строку "Hello, world\n" в стандартный поток вывода cout). Программирующим на C << известно как операция сдвига влево для целых. Такое использование << не утеряно, просто в дальнейшем << было определено для случая, когда его левый операнд является потоком вывода.

Ввод производится с помощью операции >> ("извлечь из") над стандартным потоком ввода cin. Описания cin и >>, конечно же, находятся в <stream.h>. Операцию вывода << можно применять к ее собственному результату, так что несколько команд вывода можно записать одним оператором:

cout << inch << " in = " << inch*2.54 << " cm\n";

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

вывод, но оказывается, большинство людей предпочитают, чтобы операция ввода отличалась от операции вывода. Кроме того, = не в ту сторону связывается (ассоциируется), то есть cout=a=b означает cout=(a=b).

Делались попытки использовать операции < и >, но значения "меньше" и "больше" настолько прочно вросли в сознание людей, что новые операции ввода/вывода во всех реальных случаях оказались нечитаемыми. Помимо этого, "<" находится на большинстве клавиатур как раз на ",", и у людей получаются операторы вроде такого:

cout < x , y , z;

Для таких операторов непросто выдавать хорошие сообщения об ошибках.

Операции << и >> к такого рода проблемам не приводят, они асимметричны в том смысле, что их можно проассоциировать с "в" и "из", а приоритет << достаточно низок, чтобы можно было не использовать скобки для арифметических выражений в роли операндов.

Например:

cout << "a*b+c=" << a*b+c << "\n";

Естественно, при написании выражений, которые содержат операции с более низкими приоритетами, скобки использовать надо. Например:

cout << "a^b|c=" << (a^b|c) << "\n";

Операцию левого сдвига тоже можно применять в операторе вывода:

cout << "a<<b=" << (a<<b) << "\n";

В С++ нет выражений с символьными значениями, в частности, '\n' является целым (со значением 10, если используется набор символов ASCII), поэтому

cout << "x = " << x << '\n';

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

#define sp << " "

#define ht << "\t"

#define nl << "\n"

Теперь предыдущий пример запишется в виде:

cout << "x = " << x nl;

Для печати символов предоставляются функции ostream::put(char) и chr(int). Рассмотрим примеры:

cout << x << " " << y << " " << z << "\n";

cout << "x = " << x << ", y = " << y << "\n";

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

cout << x sp << y sp << z nl;

cout << "x = " << x

    << ", y = " << y nl;

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

scanf("%d%f%c", &int_data, &float_data, &char_data);

с его эквивалентом на Си++:

cin >> int_data >> float_data >> char_data;

В Си++ не нужно заботиться о том, чтобы ставить перед переменными знак адреса &. Операция извлечения из потока сама вычисляет адреса переменных, определяет их типы и формат ввода.

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

char* oct(long, int=0);   // восьмеричное представление

char* dec(long, int=0);   // десятичное представление

char* hex(long, int=0);   // шестнадцатеричное представление

char* chr(int, int=0);     // символ

char* str(char*, int=0);  // строка

Если не задано поле нулевой длины, то будет производиться усечение или дополнение; иначе будет использоваться столько символов (ровно), сколько нужно. Например:

 cout << "dec(" << x

      << ") = oct(" << oct(x,6)

      << ") = hex(" << hex(x,4)

      << ")";

Если x==15, то в результате получится:

dec(15) = oct( 17) = hex( f);

Можно также использовать строку в общем формате:

char* form(char* format ...);

cout<<form() эквивалентно применению стандартной функции вывода языка C printf()*. form() возвращает строку, получаемую в результате преобразования и форматирования ее параметров, которые стоят после первого управляющего параметра - строки формата format. Строка формата состоит из объектов двух типов: обычных символов, которые просто копируются в поток вывода, и спецификаций преобразования, каждая из которых влечет преобразование и печать следующего из параметров. Каждая спецификация преобразования начинается с символа %. Например:

cout<<form("there were %d members present",no_of_members);

Здесь %d указывает, что no_of_members должно рассматриваться как int и печататься в виде соответствующей последовательности десятичных цифр. Если no_of_members==127, вывод будет такой:

there were 127 members present

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

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

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

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

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

* в ширине поля или точности вместо строки цифр может стоять *. В этом случае ширина поля и точность задается целым параметром;

h необязательный символ h; указывает на то, что идущие за ним d, o, x или y соответствуют параметру короткое целое;

l необязательный символ l; указывает на то, что идущие за ним d, o, x или y соответствуют параметру длинное целое;

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

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

d целый параметр преобразуется в десятичную запись;

o целый параметр преобразуется в восьмеричную запись;

x целый параметр преобразуется в шестнадцатиричную запись;

f параметр float или double преобразуется в десятичную запись вида [-]ddd.ddd, где число, задаваемое цифрами d после десятичной точки, эквивалентно спецификации точности для параметра. Если точность опущена, дается шесть цифр;

если точность явно задана как 0, то не печатается десятичная точка и не печатается ни одной цифры;

e параметр float или double преобразуется в десятичную запись вида [-]d.ddde+dd, где перед десятичной точкой стоит одна цифра, а число, задаваемое цифрами после десятичной точки, эквивалентно спецификации точности для параметра;

когда точность опущена, выдается шесть цифр;

g параметр float или double печатается в том из видов d,f или e, который обеспечивает полную точность при минимальной затрате места;

c печатается символьный параметр, пустые символы игнорируются;

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

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

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

Вот более сложный пример:

 char* src_file_name;

 int line;

 char* line_format = "\n#line %d \"%s\"\n";

 //...

 cout << "int a;\n";

 cout << form(line_format,line,src_file_name);

 cout << "int b;\n";

который печатает

int a;

 #line 13 "С++/main.c"

 int b;

Применение form() небезопасно в смысле того, что не выполняется проверка типа. Вот, например, хорошо хорошо известный способ получить непредсказуемый вывод и/или дамп (core dump):

 char x;

 // ...

 cout<<form("bad input char: %s",x);

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

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

class complex {

     float re,im;

 public:

     // ...

     char* string(char* format)

         { return form(format,re,im); }

 };

 // ...

 cout << z.string("(%.3f,%.3f)");

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

Манипуляторы. Манипуляторы - функции потока, которые можно включать в операции помещения и извлечения в потоки ( <<, >>). Имеются следующие манипуляторы:

endl            // Помещение в выходной поток символа конца строки '\n' и вызов  функции flush

ends             // Помещение в выходной поток символа '\0'

flush            // Вызов функции вывода буферизованных данных в выходной  поток

dec              // Установка основания 10 системы счисления

hex             // Установка основания 16 системы счисления

oct             // Установка основания  8 системы счисления

ws               // Установка игнорирования при вводе пробелов

setbase(int)     // Установка основания системы счисления (0 - 10 - по  умолчанию, также 8,10,16)

resetiosflasg(long) // Сброс флагов форматирования по маске

setiosflags(long) // Установка флагов форматирования по маске

setfill(int)     // Установка заполняющего символа

setprecision(int) // Установка точности вывода вещественных чисел

setw(int)        // Установка ширины поля ввода-вывода

Пример вызова манипулятора:

cout << 15 << hex << 15 << setbase(8) << 15;

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

Поток может находиться в одном из следующих состояний:

enum stream_state { _good, _eof, _fail, _bad };

Если состояние _good или _eof, значит последняя операция ввода прошла успешно. Если состояние _good, то следующая операция ввода может пройти успешно, в противном случае она закончится неудачей. Другими словами, применение операции ввода к потоку, который не находится в состоянии _good, является пустой операцией. Если делается попытка читать в переменную v, и операция оканчивается неудачей, значение v должно остаться неизменным (оно будет неизменным, если v имеет один из тех типов, которые обрабатываются функциями членами istream или ostream). Отличие между состояниями _fail и _bad очень незначительно и представляет интерес только для разработчиков операций ввода. В состоянии _fail предполагается, что поток не испорчен и никакие символы не потеряны. В состоянии _bad может быть все что угодно.

Состояние потока можно проверять например так:

switch (cin.rdstate()) {

 case _good: // последняя операция над cin прошла успешно

       break;

 case _eof: // конец файла

       break;

 case _fail: // некоего рода ошибка форматирования, возможно, не слишком плохая

       break;

 case _bad: // возможно, символы cin потеряны

       break;

 }

Для любой переменной z типа, для которого определены операции << и >>, копирующий цикл можно написать так:

while (cin>>z) cout << z << "\n";

Например, если z - вектор символов, этот цикл будет брать стандартный ввод и помещать его в стандартный вывод по одному слову (то есть, последовательности символов без пробела) на строку.

Когда в качестве условия используется поток, происходит проверка состояния потока, и эта проверка проходит успешно (то есть, значение условия не ноль) только если состояние _good. В частности, в предыдущем цикле проверялось состояние istream, которое возвращает cin>>z. Чтобы обнаружить, почему цикл или проверка закончились неудачно, можно исследовать состояние.

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

Файловый ввод-вывод с применением потоков С++. Конструкторы файловых потоков. Потоки обычно связаны с файлами. Библиотека потоков создает стандартный поток ввода cin, стандартный поток вывода cout и стандартный поток ошибок cerr. Программист может открывать другие файлы и создавать для них потоки.

Для инициализации потоков вывода ostream имеет конструкторы:

class ostream {

     // ...

     ostream(streambuf* s); // связывает с буфером потока

     ostream(int fd); // связывание для файла

     ostream(int size, char* p); // связывает с вектором

 };

Главная работа этих конструкторов - связывать с потоком буфер. streambuf - класс, управляющий буферами, как и класс filebuf, управляющий streambuf для файла. Класс filebuf является производным от класса streambuf.

Естественно, тип istream, так же как и ostream, снабжен конструктором:

class istream {

     // ...

     istream(streambuf* s, int sk =1, ostream* t =0);

     istream(int size, char* p, int sk =1);

     istream(int fd, int sk =1, ostream* t =0);

 };

Параметр sk задает, должны пропускаться пропуски или нет. Параметр t (необязательный) задает указатель на ostream, к которому прикреплен istream. Например, cin прикреплен к cout; это значит, что перед тем, как попытаться читать символы из своего файла, cin выполняет cout.flush(); - пишет буфер вывода

С помощью функции istream::tie() можно прикрепить (или открепить, с помощью tie(0)) любой ostream к любому istream.

Например:

int y_or_n(ostream& to, istream& from)

     /*"to", получает отклик из "from"  */

 {

     ostream* old = from.tie(&to);

     for (;;) {

         cout << "наберите Y или N: ";

         char ch = 0;

         if (!cin.get(ch)) return 0;

if (ch != '\n') { // пропускает остаток строки

             char ch2 = 0;

             while (cin.get(ch2) && ch2 != '\n') ;

         }

         switch (ch) {

         case 'Y':

         case 'y':

         case '\n':

             from.tie(old); // восстанавливает старый tie

             return 1;

         case 'N':

         case 'n':

from.tie(old); // восстанавливает старый tie

             return 0;

         default:

             cout << "извините, попробуйте еще раз: ";

         }

     }

 }

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

Символ можно вернуть в поток с помощью функции istream::putback(char). Это позволяет программе "заглядыватьвперед" в поток ввода.

Деструктор для ostream сбрасывает буфер с помощью открытого члена функции ostream::flush():

ostream::~ostream()

 {

     flush(); // сброс

 }

Сбросить буфер можно также и явно. Например:

 cout.flush();

Открытие файлов в разных режимах. Точные детали того, как открываются и закрываются файлы, различаются в разных операционных системах. Поскольку после включения <stream.h> становятся доступны cin, cout и cerr, во многих (если не во всех) программах не нужно держать код для открытия файлов. Вот, однако, программа, которая открывает два файла, заданные как параметры командной строки, и копирует первый во второй:

#include <stream.h>

 void error(char* s, char* s2)

 {

     cerr << s << " " << s2 << "\n";

     exit(1);

 }

 main(int argc, char* argv[])

 {

     if (argc != 3) error("неверное число параметров","");

filebuf f1;

     if (f1.open(argv[1],input) == 0)

         error("не могу открыть входной файл",argv[1]);

istream from(&f1);

     filebuf f2;

     if (f2.open(argv[2],output) == 0)

         error("не могу создать выходной файл",argv[2]);

     ostream to(&f2);

     char ch;

     while (from.get(ch)) to.put(ch);

     if (!from.eof() !! to.bad())

         error("случилось нечто странное","");

 }

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

  1.  сначала создается буфер (здесь это делается посредством описания filebuf);
  2.  затем к нему подсоединяется файл (здесь это делается посредством открытия файла с помощью функции filebuf::open()); 
  3.  создается сам ostream с filebuf в качестве параметра.  

Потоки ввода обрабатываются аналогично.

Файл может открываться в одной из двух мод:

enum open_mode { input, output };

Действие filebuf::open() возвращает 0, если не может открыть файл в соответствие с требованием. Если пользователь пытается открыть файл, которого не существует для output, он будет создан.

Используя классы ifstream и ofstream - производные от istream и ostreasm, описанные в fstream.h, можно открывать файловые потоки в разных модах с помощью флагов конструктора потока:

ofstream object (filename, flag)

где flag может иметь следующие значения:

ios::app запись в конец существующего файла

ios::ate после открытия файла перейти в его конец

ios::binary открыть файл в двоичном режиме (по умолчанию - текстовый)

ios::in открыть для чтения

ios::nocreate сообщать о невозможности открытия, если файл не существует

ios::noreplace сообщать о невозможности открытия, если файл существует

ios::out открыть для вывода

ios::trunc если файл существует, стереть содержимое.

При необходимости изменить способ открытия или применения файла можно при создании файлового потока использовать два флага или более флага: ios::app|ios::noreplace

Для открытия файла одновременно на чтение и запись можно использовать объекты класса fstream:

fstream object(filename, ios::in|ios::app);

Перед завершением программа проверяет, находятся ли потоки в приемлемом состоянии. При завершении программы открытые файлы неявно закрываются. Для явного закрытия объектов файловых потоков применяется метод close():

object.close();

Ввод-вывод в файлы. Для ввода/вывода в потоковые объекты можно применять методы put(), get(), для связывания объекта с различными файлами служат методы open(), close(), для позиционирования в файле имеются методы seekg(), seekp(), tellp(). При этом seekg() назначает или возвращает текущую позицию указателя чтения, а seekp() назначает или возвращает текущую позицию указателя записи. Обе функции могут иметь один или два аргумента. При вызове с одним аргументом функции перемещают указатель на заданное место, а при вызове с двумя аргументами вычисляется относительная позиция от начала файла (ios::beg), текущей позиции (ios::cur) или от конца файла (ios::end). Текущщая позиция определяется методом tellp().

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

bad() возвращает ненулевое значение, если обнаружена ошибка

clear() сбрасывает сообщения об ошибках

eof() возвращает ненулевое значение, если обнаружен конец файла

fail() возвращает ненулевое значение, если операция завершилась неудачно

good() возвращает ненулевое значение, если флаги ошибок не выставлены

rdstate() возвращает текущее состояние флагов ошибок.

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

7. Параметризованные типы и функции.(2 час.)

Шаблоны функций. Параметры шаблонов. Шаблоны классов. Наследование и шаблоны.

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

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

В определении шаблона семейства функций используется служебное слово template. Для параметризации используется список формальных параметров шаблона, который заключается в угловые скобки <>. Каждый формальный параметр шаблона обозначается служебным словом class, за которым следует имя параметра (идентификатор). Пример определения шаблона функций, вычисляющих абсолютные значения числовых величин разных типов:

template <class type>

type abs (type x) { return x > 0 ? x: -x;}

Шаблон семейства фукций состоит из двух частей - заголовка шаблона:

template <список_параметров_шаблона>

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

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

template <class T>

void swap (T* x, T* y)

{ T z = *x;

 *x = *y; *y = x; }

Здесь параметр T шаблона функций используется не только в заголовке для спецификации формальных параметров, но и в теле определения функции, где он задает тип вспомогательной переменной z.

Шаблон семейства функций служит для автоматического формирования конкретных определений функций по тем вызовам, которые транслятор обнаруживает в теле программы. Например, если программист употребляет обращение abs(-10.3), то на основе приведенного ранее шаблона компилятор сформирует такое определение функции:

double abs (double x) {return x > 0 ? x: -x;}

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

Если в программе присутствует приведенный ранее шаблон семейства функций swap() и появится последовательность операторов:

long k = 4, d = 8, swap (&k, &d);

то компилятор сформирует определение функции:

void swap (long* x, long* y)

{ long x = *x;

 *x = *y; *y = x; }

Затем будет выполнено обращение именно к этой функции и значения переменных k, d  поменяются местами.

Если в той же программе присутствуют операторы:

double a = 2.44, b = 66.3; swap (&a, &b);

то сформируется и выполнится функция

void swap (double* x, double* y)

{ double x = *x;

 *x = *y; *y = x; }

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

#include <iostream.h>

//Функция определяет ссылку на элемент с максимальным значением

templat <class type>

type& rmax (int n, type d[])

{ int im = 0;

 for (int i = 1; i < n; i++)

im = d[im] > d[i] ? im: i;

 return d[im]; }

void main ()

{ int n = 4;

 int x[] = { 10, 20, 30, 14}; //Массив целых чисел

 cout << "\nrmax(n,x) = " << rmax (n,x); // rmax(n,x) = 30

 rmax(n,x) = 0;

 for (int i = 0; i < n; i++)

cout << "\tx[" << i << "] =" << x[i]; // x[0] = 10 x[1] ...

 float arx[] = { 10.3, 20.4, 10.5}; //Массив вещественных чисел

 cout << "\nrmax(3,arx) = " << rmax (3,arx); //rmax(3,arx) = 20.4

 rmax(3, arx) = 0;

 for (int i = 0; i < 3; i++)

cout << "\tarx[" << i << "] =" << arx[i]; //arx[0] = 10.3 ...

}

В программе используются два разных обращения к функции rmax(). В одном случае параметр - целочисленный массив и возвращаемое значение - ссылка типа int. Во втором случае фактический параметр - имя массива типа float и возвращаемое значение имеет тип ссылки на float.

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

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

Перечислим основные свойства параметров шаблона:

  1.  Имена параметров шаблона должны быть уникальными во всем определении шаблона.
  2.  Список параметров шаблона функции не может быть пустым, так как при этом теряется возможность параметризации и шаблон функций становится обычным определением конкретной функции.
  3.  В списке параметров шаблона функций может быть несколько параметров. Каждый из них должен начинаться со служебного слова class. Например, допустим такой заголовок шаблона:

template <class type1, class type2>

Соответственно, неверен заголовок:

template <class type1, class type2>

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

#include <iostream.h>

int N; //статическая, инициализирована нулем

template <class N>

N max (N x, N y)

{ N a = x;

 cout << "\nСчетчик обращений N = " << ++::N;

 if (a < y) a = y;

 return a:

}

void main ()

{ int a = 12, b = 42;

 max (a,b);  //Счетчик обращений N = 1

 float z = 66.3, f = 222.4;

 max (z,f);  //Счетчик обращений N = 2

}

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

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

template <class A, class B, class C>

B func (A n, C m) {B valu; ... }

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

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

#include <iostream.h>

template <class D>

long count0 (int, D *); //Прототип шаблона

viod main ()

{ int A[] = {1,0,6,0,4,10};

 int n = sizeof(A)/sizeof A[0];

 cout << "\ncount0(n,A) = " << count0(n,A);

 float X[] = {10.0, 0.0, 3.3, 0.0, 2.1};

 n = sizeof(X)/sizeof X[];

 cout << "\ncount0(n,X) = " << count0(n,X);

}

template <class T>

long count0 (int size, T* array)

{ long k = 0;

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

        if (int(array[i]) == 0) k++;

 return k;

}

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

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

template  < список_ параметров_ шаблона >

В списке параметров прототипа шаблона имена параметров не обязаны совпадать с именами тех же параметров в определении шаблона. Это и продемонстрировано в программе.

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

template < class E > void swap (E,E);

недопустимо использовать такое обращение к функции:

int n = 4; double d = 4.3;

swap (n,d); // Ошибка в типах параметров

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

swap (double (n) , d); // Правильные типы параметров

приведет к кокретизации шаблонного определения функций с параметром типа double.

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

Шаблоны классов. Аналогично шаблонам функций. определяется шаблон семейства классов:  template <список_параметров_шаблона> определение_класса

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

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

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

Следующий шаблон автоматически формирует классы векторов с указанными свойствами:

// TEMPLATE.VEC - шаблон векторов

template <class T> // T -  параметр шаблона

ckass Vector

{ T *data;              // Начало одномерного массива

 int size;              // Количество элементов в иассиве

 public:

       Vector (int);    // Конструктор класса vector

      Vector () { delete [] data; }         // Деструктор

     // Расширение действия (перегрузка) операции «[]»:

    T& operator [] (int i) { return data [i];}

};

// Внешнее определение конструктора класса:

template <class T>

Vector <T>:: Vector (int n)

{ data = new T [n];

 size = n; };

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

имя_параметризованного_класса <фактические_параметры_шаблона>

   имя_объекта (параметры_конструктора);

В нашем случае определить вектор, имеющий восемь вещественных координат типа double, можно следующим образом: Vector <double> z (8);

Проиллюстрируем сказанное  следующей программой:

// формирование классов с помощью шаблона

# inclub «template.vec» // Шаблон класса «вектор»

# inclub <iostream.h>

main ()

{Vector <int> X(5); //Создаем объект класса «целочисленный вектор»

 Vector <char> C(5); // Создаем объект класса «символьный вектор»

for (int i = 0; i < 5; i++)// Определяем компоненты векторов

   { X [i] =  i; C [i] = ‘A’ + i;}

for (i = 0; i < 5; i++)

   cout <<     << X[i] <<    << C[i];}// 0 A   1 B    2 C    3 D    4 E

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

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

# include <iostream.h>

template <class T, int size = 64>

class row { T *data;

  int length;

public: row ()

       { length = size;

         data = new T [size];

       }

      ~row ()  { delete [] data; }

      T& operator [] (int i)

     { return data [i]; }

};

void main ()

{ row <float,8> rf;

 row <int,8> ri;

for (int i = 0; i < 8; i++)

    { rf [i] = i; ri [i] = i * i; }

for (i = 0; i < 8, i++)

 cout <<   << rf[i] <<   << ri[i]; }  //0 0  1 1  2 4  3 9  4 16  5 25  6 36  7 49

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


 

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

29973. Методология и методы исследования в психологии 71 KB
  Методология и методы исследования в психологии Вступление: об актуальности изучения методологии развитие науки междисциплинарные связи новые исследования в больших коллективах – требуют единого понимания методов. Методология науки – учение о методах и принципах познания обеспечивающих путь к объективной истине это учение о структуре логической организации методах и средствах Дружинин В. предмет – саморефлексия науки а именно изучение методологических принципов познания позволяющих решать ключевые психологич проблемы психофизич...
29974. Развитие психики в процессе эволюции животного мира 48.5 KB
  Развитие психики в процессе эволюции животного мира Психика – системное свойство высокоорганизованной материи заключается в активном отражении субъектом объективного мира в построении картины мира и саморегуляция на этой основе своего поведения и деятельности. Зоопсихизм –путь к изучению психики ч з изучение поведения. Низший уровень простейшие: преобладают инстинктивные формы поведения. В основном форма поведения – таксисы простейшие движения у некоторых появляется подобие брачных игр черви.
29975. ВПФ по Л.С. Выготскому. Их происхождение, свойства, генезис 57 KB
  Выготский создал культурноисторическую теорию психики человека. Иногда ее называют теорией общественноисторического происхождения высших психических функций человека. 1 измененное взаимоотношение человека и природы. 2 высшие появляются у человека произвольные: человек может заставить себя запомнить некоторый материал обратить внимание на какойто предмет организовать свою умственную деятельность.
29976. ПРОБЛЕМА СОЗНАНИЯ В ПСИХОЛОГИИ 147 KB
  Поэтому в психологии говорят о единстве сознания челка и его деятельности. Явления сознания как предмет интроспективной психологии XIX века. Существование сознания главный и безусловный факт а задача психологии – проанализировать состояния и содержание сознания предмет сознание.
29977. Классификация неосознаваемых явлений 56 KB
  Активность монад протекающая в сфере психических актов имеет различную степень сознательности: от почти полностью бессознательного до ясного и четкого сознания. Низшие уровни сознания называются малыми перцепциями их сознательная реализация получила названия апперцепции. Он сформулировал концепцию порога сознания. Порог сознания уровень психической деятельности ниже которого идеи оказываются бессознательными.
29978. Методы неосознаваемых явлений в психоанализе 55 KB
  предсознательное скрытое латентное бессознательное потенциально сознательное: может проникнуть в сознание т. бессознательное вытесненная бессознательная психика не обладает способностью проникнуть в сознание: это может только представитель вытесненной бессознательной психики. Бессознательное это место сосредоточения влечений все вытесненное из сознания как недопустимое па своей природе.Фрейда Под влиянием цензуры происходит вытеснение идеи с которой связано несовместимое желание в...
29979. Психологическая теория деятельности в отеч.психологии 56 KB
  Психологическая теория деятельности в отеч. придумал Леонтьев Психологическая теория деятельности начала разрабатываться в 20х начале 30х гг. Но главное состояло в том что авторы теории деятельности взяли на вооружение философию диалектического материализма теорию К. Этот общий философский тезис нашел в теории деятельности конкретнопсихологическую разработку.
29980. Понятие действия. Виды. Соотношение действий и Деятельности 43 KB
  Понятие действия. того результата который должен быть достигнут в ходе выполнения действия. Характеристики действия: действие включает в качестве необходимого компонента акт сознания в виде постановки и удержания цели. через понятие действия теория деятельности утверждает принцип активности точка анализа Д – субъект.
29981. Ощущения. Общая характеристика 64.5 KB
  Ощущения. Физиологическая основа ощущения. Физиологической основой ощущения является нервный процесс возникающий при действии раздражителя на адекватный ему анализатор. сигнализируют о движениях кинестетические ощущения ощущение равновесия статические ощущения.