36627

ПРОГРАММИРОВАНИЕ. Курс лекций

Конспект

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

Понятия объекта класса объектов. Доступность компонентов класса. Статические и константные компоненты класса. Указатели на компоненты класса.

Русский

2013-09-23

1.11 MB

8 чел.

ПРОГРАММИРОВАНИЕ (2 семестр)

[0.1] 1. Технологии программирования.

[0.1.1] 1.1 Введение.

[0.1.2] 1.2 Модульное программирование.

[0.1.3] 1.3 Нисходящее программирование.

[0.1.4] 1.4 Структурное программирование.

[0.1.5] 1.5 Понятия объекта, класса объектов.

[0.1.6] 1.6 Основные понятия объектно-ориентированного программирования: инкапсуляция, наследование и полиморфизм.

[0.2] 2. Основы программирования на языке С++

[0.3] 2.1. Лексические основы языка С++. Общие сведения о программах, лексемах и алфавите языка. Идентификаторы и служебные слова.

[0.3.1] 2.1.1 Общие сведения о программах, лексемах и алфавите языка

[0.3.2] 2.1.2. Алфавит и лексемы языка СИ++.

[0.3.3] 2.1.3. Идентификаторы и служебные слова

[0.4] 3. Константы: целые, вещественные (с плавающей точкой), перечислимые, символьные (литерные), строковые (строки или литерные строки)

[0.5] 4. Операции. Знаки операций. Унарные, бинарные и тернарные операции. Приоритеты операций.

[0.5.1] 4.1 Знаки операций

[0.5.2] 4.2 Унарные операции

[0.5.3] 4.3 Бинарные операции.

[0.5.4] 4.4 Приоритеты операций

[0.6] 5. Переменные. Определения и описания. Спецификатор typedef.

[0.6.1] 5.1 Переменные. Определения и описания.

[0.6.2] 5.2 Класс памяти

[0.7] 6. Базовые и производные типы данных. Массивы. Указатели, ссылки и адреса. Структуры. Поля битов. Объединения

[0.7.1] 6.1 Массивы

[0.7.2] 6.2 Указатели, ссылки и адреса объектов

[0.7.3] 6.3 Структуры

[0.7.4] 6.4 Поля битов

[0.7.5] 6.5 Объединения

[0.8] 7. Операторы

[0.8.1] 7.1 Оператор выражение

[0.8.2] 7.2 Пустой оператор

[0.8.3] 7.3 Составной оператор

[0.8.4] 7.4 Оператор if

[0.8.5] 7.5 Оператор switch

[0.8.6] 7.6 Оператор break

[0.8.7] 7.7 Оператор for

[0.8.8] 7.8 Оператор while

[0.8.9] 7.9 Оператор do while

[0.8.10] 7.10 Оператор continue

[0.8.11] 7.11 Оператор return

[0.8.12] 7.12 Оператор goto

[0.9] 8. Функции

[0.9.1] 8.1 Определения, описания и вызовы функций

[0.9.2] 8.2 Начальные (умалчиваемые) значения параметров.

[0.9.3] 8.3 Функции с переменным количеством параметров

[0.9.4] 8.4 Перегрузка функций.

[0.9.5] 8.5 Ссылки и параметры-ссылки.

[0.9.6] 8.6 Шаблоны функций.

[0.10] Основы ООП

[0.11] 9. Классы  С++

[0.11.1] 9.1 Тип данных - класс.

[0.11.2] 9.2 Доступность компонентов класса

[0.11.3] 9.3 Конструктор и деструктор

[0.11.4] 9.4 Компоненты-данные и компоненты-функции. Статические и константные компоненты класса

[0.12] 10. Указатели на компоненты класса

[0.12.1] 10.1 Указатели на компоненты- данные.

[0.12.2] 10.2 Указатели на компоненты- функции.

[0.12.3] 10.3 Указатель this

[0.13] 11. Друзья классов

[0.13.1] 11.1 Дружественная функция

[0.13.2] 11.2 Дружественный класс

[0.14] 12. Наследование

[0.14.1] 12.1 Определение производного класса.

[0.14.2] 12.2 Конструкторы и деструкторы производных классов

[0.15] 13. Полиморфизм

[0.15.1] 13.1 Виртуальные функции.

[0.15.2] 13.2 Абстрактные классы

[0.16] 14. Шаблоны классов

[0.17] 15. Перегрузка операций

[0.17.1] 15.1 Общие сведения о перегрузке стандартных операций

[0.17.2] 15.2 Перегрузка унарных операций

[0.17.3] 15.3 Перегрузка бинарных операций

[0.17.4] 15.4 Перегрузка операций ++ и --.

[0.17.5] 15.5 Перегрузка операции вызова функции

[0.17.6] 15.6 Перегрузка операции присваивания

[0.17.7] 15.7 Основные правила перегрузки операций.

[0.18] 16. Обработка исключительных ситуаций

[0.18.1] 16.1 Операторы try, throw, catch

[0.18.2] 16.2 Универсальный обработчик исключений

[0.19] 17. Структура Windows-приложения

[0.19.1] 17.1 Разработка Windows – приложений на языке С++

[0.19.2] 17.2 Структура каркасного Windows-приложения

[0.19.3] 17.3 Главная функция WinMain()

[0.19.4] 17.4 Сообщения Windows

[0.19.5] 17.5 Класс окна. Регистрация и его характеристики

[0.19.6] 17.6 Создание и показ окна

[0.19.7] 17.7 Цикл обработки сообщений

[0.19.8] 17.8 Оконная функция

[0.19.9] 17.9 Завершение выполнения приложения

[0.20] 18. Препроцессор

[0.20.1] 18.1 Общие пpеобpазования

[0.20.2] 18.2 Директивы Препроцессора

[0.20.3] 18.3 Подключаемые файлы

[0.20.4] 18.4. Директива '#include'.

[0.20.5] 18.5 Однократно подключаемые файлы

[0.20.6] 18.6 Макросы

[0.20.7] 18.7 Стрингификация

[0.20.8] 18.8 Объединение

[0.20.9] 18.9 Удаление макросов

[0.20.10] 18.10 Условия

[0.21] 19. Разработка Windows приложений с использованием библиотеки
классов MFC (microsoft foundation class library)

[0.21.1] 19.1 Некоторые сведения о программировании Windows-приложений

[0.21.2] 19.2 Преимущества использования MFC

[0.21.3] 19.4 Библиотека MFC

[0.22] 20. Простейшие MFC-приложения

[0.22.1] 20.1 Приложение без главного окна

[0.22.2] 20.2 Приложение с главным окном

[0.22.3] 20.3 Обработка окном сообщений

1. Технологии программирования.

1.1 Введение.

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

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

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

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

Парадигма программирования.

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

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

1.2 Модульное программирование.

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

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

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

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

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

1.3 Нисходящее программирование.

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

  •  простой последовательности действий;
  •  конструкции выбора или оператора if;
  •  конструкции повторения или цикла.

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

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

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

Пошаговое программирование.

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

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

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

1.4 Структурное программирование.

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

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

1.5 Понятия объекта, класса объектов.

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

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

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

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

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

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; // операций

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

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

1.6 Основные понятия объектно-ориентированного программирования: инкапсуляция, наследование и полиморфизм.

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

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

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

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

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

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

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

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

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

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

2. Основы программирования на языке С++

Язык программирования Си++ был разработан на основе языка Си Бьярном Страуструпом (Bjarne Stroustrup) и вышел за пределы его исследовательское группы в начале 80-х годов. На первых этапах разработки (1980г.) язык носил условное название "Си с классами", а в 1983 г. Рик Масситти придумал название "Си++'', что образно отразило происхождение этого нового языка от языка Си. Язык Си++ является расширением (надмножеством) языка Си, поэтому программы, написанные на Си, могут обрабатываться компилятором языка Си++. Более того, в программах на языке Си++ можно использовать тексты на языке Си и обращаться к библиотечным функциям языка Си. Таким образом, одно из достоинств Си++ состоит в возможности использовать уже существующие программы на Си. Однако это не единственное достоинство языка. Язык Си++ был создан с учетом следующих целей: улучшить язык Си, поддержать абстракцию данных и обеспечить объектно-ориентированное программирование.

Через несколько лет его практического использования стандартом де-факто стала спецификация языка AT&T C++ release 2.0, разработанная в Bell Laboratories фирмы AT&T под руководством автора языка Б. Страуструпа. Затем там же появилась усовершенствованная версия 3.0 языка Си++. В настоящее время в Американском национальном институте Стандартов (ANSI) существует комитет по языку Си++. Изданное в 1990 году описание языка с комментариями  принято комитетом ANSI в качестве исходного материала для стандартизации Си++. Весьма полная реализация соглашений по языку Си++ выполнена в широко распространенных компиляторах Turbo C++ и Borland C++ фирмы Borland,  Microsoft Visual C++ фирмы Microsoft.

2.1. Лексические основы языка С++. Общие сведения о программах, лексемах и алфавите языка. Идентификаторы и служебные слова.

2.1.1 Общие сведения о программах, лексемах и алфавите языка

Следуя классикам, начнем с примера программы, выводящей на экран фразу "Hello, World!"

// HELLO.CPP - имя файла с программой

#include <iostream.h>

void main() {

cout << "\nHello, World!\n";

}

Основная программная единица на языке Си++ - это текстовый файл с названием <имя>.срр, где cpp - принятое расширение для программ на Си++, а имя выбирается достаточно произвольно. Для удобства ссылок и сопоставления программ с их внешними именами целесообразно помещать в начале текста каждой программы строку комментария с именем файла, в котором она находится. Текстовый файл с программой на Си++ вначале обрабатывает препроцессор, который распознает команды (директивы) препроцессора (каждая такая команда начинается с символа '#') и выполняет их. В приведенной выше программе использована препроцессорная команда include <имя вставляемого файла>

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

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

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

/* Это комментарий */

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

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

// Это однострочный комментарий, специфичный для языка Си++

2.1.2. Алфавит и лексемы языка СИ++. 

В алфавит языка Си++ входят

  •  прописные и строчные буквы латинского алфавита;
  •  цифры 0, 1, 2, 3, 4, 5, 6 7, 8, 9;
  •  специальные знаки: " { } , | [ ] ( ) + - / % \ ; ' : ? < = > _ ! & # ~ ^ . *

Из символов алфавита формируются лексемы языка:

  •  идентификаторы;
  •  ключевые (служебные, иначе зарезервированные) слова;
  •  константы;
  •  знаки операций;
  •  разделители (знаки пунктуации).

2.1.3. Идентификаторы и служебные слова

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

RON   run   hard_RAM_disk   copy_54

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

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

asm  double  new  switch

auto  else  operator template

break enum  private  this

case  extern  protected throw

catch float  public  try

char  for  register  typedef

class friend  return  typeid

const goto  short  union

continue if  signed  unsigned

default inline  sizeof  virtual

delete int  static  void

do  long  struct  volatile  while

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

3. Константы: целые, вещественные (с плавающей точкой), перечислимые, символьные (литерные), строковые (строки или литерные строки)

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

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

Компилятор, выделив константу в качестве лексемы, относит её к той или другой группе, а внутри группы - к тому или иному типу данных по ее "внешнему виду" (по форме записи) в исходном тексте и по числовому значению.

Целая константа: это десятичное, восьмеричное или шестнадцатеричное число, которое представляет целую величину в одной из следующих форм: десятичной, восьмеричной или шестнадцатеричной.

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

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

Шестнадцатеричная константа начинается с обязательной последовательности 0х или 0Х и содержит одну или несколько шестнадцатеричных цифр (цифры представляющие собой набор цифр шестнадцатеричной системы счисления: 0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F)

     Примеры целых констант:

        Десятичная      Восьмеричная       Шестнадцатеричная

        константа         константа              константа

            16                  020                         0x10

           127                 0117                       0x2B

           240                 0360                       0XF0

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

Например: -0x2A, -088, -16 .

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

- десятичные константы рассматриваются как величины со знаком, и им присваивается тип int (целая) или long (длинная целая) в соответствии со значением константы.

- восьмеричным и шестнадцатеричным константам присваивается тип int, unsigned int (беззнаковая целая), long или unsigned long в зависимости от значения константы.

Если программиста по каким-либо причинам не устраивает тот тип, который компилятор приписывает константе, то он может явным образом повлиять на его выбор. Для этого служат суффиксы L, l (long) и U, u (unsigned). Например, константа 64L будет иметь тип long, хотя значению 64 должен быть приписан тип int. Для одной константы можно использовать два суффикса U(u) и L(l), причем в произвольном порядке. Например, константы Ох22Ul, Ox16Lu будут иметь тип unsigned long.

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

  •  целая часть (десятичная целая константа);
  •  десятичная точка;
  •  дробная часть (десятичная целая константа);
  •  признак (символ) экспоненты е или Е;
  •  показатель десятичной степени (десятичная целая константа, возможно со знаком);
  •  суффикс F(или f – одинарная точность) либо l (или 1 – удвоенная точность).

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

66.

.0

.12

3.14159F

2E+6L

2.71

1.12е-2

При отсутствии суффиксов f (f) или L (1) вещественные константы имеют форму внутреннего представления, которой в языке Си++ соответствует тип данных double. Добавив суффикс f или F, константе придают тип float. Константа имеет тип long double, если в ее представлении используется суффикс L или 1.

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

enum { one= 1, two = 2, three = 3 };

Здесь enum - служебное слово, определяющее тип данных "перечисление", one, two, three - условные имена, введенные программистом для обозначения констант 1,2,3. После такого определения в программе вместо константы 2 (и наряду с ней) можно использовать ее обозначение two и т.д.

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

enum { zero, one, two, three };

перечислимые константы примут значения:

zero=0, one=1, two=2, three=3

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

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

Так как отрицательная целая константа - это константа без знака, к которой применена унарная операция "-" (минус), то перечислимые константы могут иметь и отрицательные значения.

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

enum Numbers {zero, one, two, three };

Символьные (литерные) константы - это один или два символа, заключенные в апострофы. Односимвольные константы имеют стандартный тип char. Для представления их значений могут вводится переменные символьного типа, т.е. типа char. Примеры констант: 'z', '*', '\012', '\0', '\n', '\х07' - односимвольные константы, 'db', '\х07\х07', '\n\f' - двухсимвольные константы. В этих примерах заслуживают внимания последовательности, начинающиеся со знака '\'. Символ обратной косой черты '\' используется, во-первых, при записи кодов, не имеющих графического изображения, и, во-вторых, символов апостроф (\'), обратная косая черта (\\), знак вопроса (\?) и кавычки (\'). Кроме того, обратная косая черта позволяет вводить символьные константы, явно задавая их коды в восьмеричном или шестнадцатеричном виде. Последовательности литер, начинающиеся со знака '\', называют эскейп-последовательностями.

Допустимые ESC-последовательности в языке С++

Изображение

Внутренний код

Реакция или смысл

\a

0x07

Звуковой сигнал

\b

0x08

Возврат на шаг (забой)

\f

0x0C

Перевод страницы (формата)

\n

0x0A

Перевод строки (новая строка)

\r

0x0D

Возврат каретки

\t

0x09

Табуляция горизонтальная

\v

0x0B

Табуляция вертикальная

\\

0x5C

Обратная косая черта

\'

0x27

Апостроф (одинарная кавычка)

\"

0x22

Двойная кавычка

\?

0x3F

Вопросительный знак

\000

000

Восьмеричный код символа

\xhh

0xhh

Шестнадцатеричный код символа

Значением символьной константы является числовое значение ее внутреннего кода. Как упоминалось, в Си++ односимвольная константа имеет тип char, т.е. занимает в памяти 1 байт. Двухсимвольные константы вида '\t\n' или \n\07' представляются двухбайтовыми значениями типа int.

Строка, или строковая константа, иногда называемая литерной строкой, определяется как последовательность символов, заключенная в кавычки (не в апострофы):

"Это строка, называемая также строковой константой"

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

Строка - это массив символов. Строка имеет тип char[].

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

Размещая строку в памяти, транслятор автоматически добавляет в ее конец символ '\0', т.е. нулевой байт. Таким образом, количество символов во внутреннем представлении строки на 1 больше числа символов в ее записи. Пустая строка хранится как один символ "\0".

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

Кавычки не входят в строку, а служат ее ограничителями при записи в программе. В строке может быть один символ, например, "а" - строка из одного символа. Однако в отличие от символьной константы 'а' длина внутреннего представления строки "а" равна 2. Строка может быть пустой "", при этом ее длина равна 1. Однако символьная константа не может быть пустой, т.е. запись '' в большинстве реализации недопустима.

4. Операции. Знаки операций. Унарные, бинарные и тернарные операции. Приоритеты операций.

4.1 Знаки операций

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

В ANSI-стандарте языка Си определены следующие знаки операций

[]

()

.

->

++

--

&

*

+

-

~

~

sizeof

/

%

<<

>>

<

>

<=

>=

==

!=

^

|

&&

||

?:

=

*=

/=

%=

+=

-=

<<=

>>=

&=

^=

|=

,

#

##

Дополнительно к перечисленным в С++ введены

::

.*

->*

new

delete

typeid

За исключением операции [], () и ?: все знаки операций распознаются компилятором как отдельные лексемы. В зависимости от контекста одна и та же лексема может обозначать разные операции.

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

4.2 Унарные операции

&

Oперация получения адреса операнда

int i;

int *i_ptr;

i_ptr = &i

*

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

*i_ptr = 10; // i = 10

-

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

+

унарный плюс (введен для симметрии с унарным минусом);

~

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

!

логическое отрицание (НЕ) значения операнда; применяется к скалярным операндам; целочисленный результат 0 (если операнд ненулевой, т.е. истинный) или 1 (если операнд нулевой, т.е. ложный). В качестве логических значений в языке Си++ используют целые числа: 0 - ложь и не нуль (!0) - истина. Отрицанием любого ненулевого числа будет 0, а отрицанием нуля будет 1. Таким образом: !l равно 0; !2 равно 0; !(-5) равно 0; !0 равно 1;

++

увеличение на единицу (инкремент или автоувеличение):

префиксная операция - увеличение значения операнда на 1 до его использования;

постфиксная операция - увеличение значения операнда на 1 после его использования.

Операнд не может быть константой либо другим праводопустимым выражением. Записи ++5 или 84++ будут неверными. Операндом не может быть и произвольное выражение. Например, ++(j+k) также неверная запись. Операндом унарных операций ++ и — должны быть всегда леводопустимые выражения, например, переменные разных типов);

--

уменьшение на единицу (декремент или автоуменьшение). Правила применения такие же как и для операции ++

sizeof

операция вычисления размера (в байтах) для объекта того типа, который имеет операнд. Разрешены два формата операции:

sizeof унарное_выражение

sizeof (тип)

4.3 Бинарные операции.

Эти операции делятся на следующие группы:

  •  аддитивные;
  •  мультипликативные;
  •  сдвигов;
  •  поразрядные;
  •  операции отношений;
  •  логические;
  •  присваивания;
  •  выбора компонента структурированного объекта;
  •  операции с компонентами классов;
  •  операция "запятая";
  •  скобки в качестве операций.

Аддитивные операции:

+

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

-

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

Мультипликативные операции:

*

умножение операндов арифметического типа;

/

деление операндов арифметического типа.  При целочисленных операндах абсолютное значение результата округляется до целого. Например, 20/3;

равно 6, -20/3 равняется -6

%

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

Операции сдвига (определены только для целочисленных операндов). Формат выражения с операцией сдвига: операнд_левый операция сдвига операнд_правый

<<

сдвиг влево битового представления значения левого целочисленного операнда на количество разрядов, равное значению правого целочисленного операнда;

>>

сдвиг вправо битового представления значения левого целочисленного операнда на количество разрядов, равное значению правого целочисленного операнда

Поразрядные операции

&

поразрядная конъюнкция (И) битовых представлений значений целочисленных операндов;

|

поразрядная дизъюнкция (ИЛИ) битовых представлений значений целочисленных операндов;

^

поразрядное исключающее ИЛИ битовых представлений значений целочисленных операндов

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

//Р2.СРР - операции сдвига и поразрядные операции

#include <iostream.h>

void main()

{

cout << "\n 4<<2 равняется " << (4<<2) ;

coot << "\t 5 >>l равняется " << (5>>1) ;

coou << "\n 6&5 равняется " << (6&5);

coat << "\t 6|5 равняется " << (6|5);

coot « "\t 6^5 равняется " << (6^5);

}

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

4<<2 равняется 16 5>>1 равняется 2

6&5 равняется 4  6|5 равняется 7

6^5 равняется 3

Операции отношения (сравнения):

<

меньше, чем;

>

больше, чем

<=

меньше или равно

>=

больше или равно

==

равно;

!=

не равно;

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

Логические бинарные операции:                                     

&&

конъюнкция (И) арифметических операндов или отношений. Целочисленный результат 0 (ложь) или 1 (истина);

||

дизъюнкция (ИЛИ) арифметических операндов или отношений. Целочисленный результат 0 (ложь) или 1 (истина).

Операции присваивания

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

=

присвоить значение выражения-операнда из правой части операнду левой части: р = 10.6 - 2*х

*=

присвоить операнду левой части произведение значений обоих операндов:

p *= 2 эквивалентно p = p * 2

/=

присвоить операнду левой части частное от деления значения левого операнда на значение правого:

р /= 2.2 - d  эквивалентно р =  р / (2.2 - d);

%/

присвоить операнду левой части остаток от деления целочисленного значения левого операнда на. целочисленное значение правого операнда:

n %=3 эквивалентно n = N % 3;

+=

присвоить операнду левой части сумму значений обоих операндов:

A += B эквивалентно а = а + B;

-=

присвоить операнду левой части разность значений левого и правого операндов:

A -= B эквивалентно а = а - B;

<<=

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

а <<= 4 эквивалентно а =  a << 4;

>>=

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

b >>= 4 эквивалентно b =  b >> 4;

&=

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

c&=7 эквивалентно c =  c & 4;

|=

присвоить целочисленному операнду левой части значение полученное поразрядной дизъюнкцией (ИЛИ) его битового представления с битовым представлением целочисленного операнда правой части:

а |=b эквивалентно a =  а | b;

^=

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

z ^= х + у эквивалентно z = z ^ (х + у).

Обратите внимание, что для всех операций сокращенная форма присваивания El ор= Е2 эквивалентна E1 = E1 ор (Е2), где ор обозначение операции.

Операции выбора компонентов структурированного объекта

.*

(точка) прямой выбор (выделение) компонента структурированного объекта, например объединения. Формат применения операции:

имя_структурированного_объекта . имя_компонента

->

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

указатель_на_структурированый_объект -> имя_компонента

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

Операции разадресации(*) и адреса(&)

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

- указатель является нулевым;

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

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

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

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

Операция адрес не может применятся к элементам структуры, являющимися полями битов, и к объектам с классом памяти register.

Примеры:

     int t, f=0,  *adress;

     adress = &t; /* переменной adress, объявляемой как

                    указатель, присваивается адрес переменной t */

    *adress =f; /* переменной находящейся по адресу, содержащемуся

                    в переменной adress, присваивается значение

                    переменной f,  т.е.  0 , что эквивалентно

                    t=f;      т.е.   t=0;                       */

Операции с компонентами классов:

*

прямое обращение к компоненту класса по имени объекта и указателю на компонент;

->*

косвенное обращение к компоненту класса через указатель на объект и указатель на компонент.

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

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

::     операция указания области видимости имеет две формы: бинарную и унарную. Бинарная форма применяется для доступа к компоненту класса. Унарная операция '::' позволяет получить доступ к внешней для некоторой функции именованной области памяти.

#include <iostream.h>

int k = 15;

void main()

{

int k = 10;

cout << "Внешняя переменная k="<< ::k;

cout << "\nВнутреняя переменная k="<< ++k;

}

Операция последовательного вычисления

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

cout << "Выражение d = 4, d*2 равно "<< (d=4, d*2);  // выведет 8

cout << "d равно "<< d;      // выведет 4

Условная операция

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

операнд-1 ? операнд-2 : операнд-3

Операнд-1 должен быть целого или вещественного типа или быть указателем. Он оценивается с точки зрения его эквивалентности 0. Если операнд-1 не равен 0, то вычисляется операнд-2 и его значение является результатом операции. Если операнд-1 равен 0, то вычисляется операнд-3 и его значение является результатом операции. Следует отметить, что вычисляется либо операнд-2, либо операнд-3, но не оба.

Пример:

max = (d<=b) ? b : d;

Переменной max присваивается максимальное значение переменных d и b.

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

Круглые скобки обязательны в обращении к функции:

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

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

В операции имя_массива[индекс] операндами для операции [] служат имя массива и индекс.

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

(имя_типа) операнд

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

(long) 1 - внутреннее представление имеет длину 4 байта;

(char)1 - внутреннее представление имеет длину 1 байт.

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

имя_типа (операнд)

Она может использоваться только в тех случаях, когда тип имеет простое (несоставное) наименование (обозначение):

long (2)  - внутреннее представление имеет длину 4 байта;

double (2) - внутреннее представление имеет длину 8 байтов.

Однако будет недопустимым выражение:

unsigned long(2) // Ошибка!

Операции new, delete для динамического распределения памяти.

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

new имя_типа

или

new имя_типа (инициализатор)

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

указатель = new имя_типа (инициализатор)

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

Пример:

int *t;

double *d;

i = new int(10);

d = new double;

*d=3.14

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

delete указатель;

где указатель адресует освобождаемый участок памяти, ранее выделенный операцией new. Например, delete d; - освободит участок памяти, связанный с указателем d.

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

delete [] указатель;

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

4.4 Приоритеты операций

В языке СИ++ операции с высшими приоритетами вычисляются первыми. Наивысшим приоритетом является приоритет равный 1. Приоритеты и порядок операций приведены в таблице:

Приоритет

Знак операции

Типы операции

Порядок выполнения

1

() [] . -> ::

Выражение

Слева направо

2

! ~ + - ++ -- & * (тип) sizeof new delete тип()

Унарные

Справа налево

3

.* ->*

Слева направо

4

* / %

Мультипликативные

Слева направо

5

+ -

Аддитивные

6

<< >>

Сдвиг

7

< > <= >=

Отношение

8

== !=

Отношение (равенство)

9

&

Поразрядное И

10

^

Поразрядное исключающее ИЛИ

11

|

Поразрядное ИЛИ

12

&&

Логическое И

13

||

Логическое ИЛИ

14

? :

Условная

15

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

Простое и составное присваивание

Справа налево

16

,

Последовательное вычисление

Слева направо

5. Переменные. Определения и описания. Спецификатор typedef.

5.1 Переменные. Определения и описания.

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

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

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

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

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

• переменные;

• функции;

• классы, их компоненты и указатели на компоненты;

• типы, вводимые пользователем с помощью typedef;

• типы и имена структур, объединений, перечислений;

• компоненты (элементы) структур и объединений;

• массивы объектов заданного типа;

• перечислимые константы;

• метки операторов;

• макросы препроцессора;

• указатели на объекты или функции заданного типа;

• ссылки на объекты или функции заданного типа;

• константы (значения) заданного типа.

Определение переменных заданного типа имеет следующий формат:

[спецафикатор-класа-памяти] [модификатор] тип имя [=инициатор] [,имя [= инициатор] ]...

где

спецификатор класса памяти - (auto, static, extern, register, typedef)

модификатор - const или volatile;

тип- один из основных типов;

имя - идентификатор;

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

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

char (символьный);

short (короткий целый);

int (целый);

long (длинный целый);

float (вещественный);

double (вещественный с удвоенной точностью);

void (отсутствие значения).

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

long double zebra, atop;

вводит переменные с именами zebra и atop вещественного типа повышенной точности, но явно не присваивает этим переменным никаких начальных значений.

Употребляемые как отдельно, так и вместе с другими именами типов служебные слова unsigned (беззнаковый) и signed (знаковый) позволяют для арифметического или символьного типа выбирать способ учета знакового разряда:

unsigned int i, j // Значения от 0 до 65535

unsigned long L, It ,N; // Значения от О до 4294967265

unsigned char с, s; // Значения от 0 до 255

Применение в определениях типов отдельных служебных слов int, char, short, long эквивалентно signed int, signed char, signed short, signed long. Именно поэтому служебное слова signed обычно опускается в определениях и описаниях. Использование при задании типа только одного unsigned эквивалентно unsigned int.

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

typedef unsigned char COD;

COD simbol;

введен новый тип COD - сокращенное обозначение для unsigned char и переменная этого типа simbol, значениями которой являются беззнаковые числа в диапазоне от 0 до 255.

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

5.2 Класс памяти

Класс памяти определяет порядок размещения объекта в памяти. Различают автоматический и статический классы памяти. C++ располагает четырьмя спецификаторами класса памяти:

auto

register

static

extern

по два для обозначения принадлежности к автоматическому и статическому классам памяти.

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

Следующая таблица иллюстрирует иерархию классов памяти.

Динамический класс памяти

Статический класс памяти

Автоматический

Регистровый

Локальный

Глобальный

auto

register

static

extern

Спецификаторы позволяют определить класс памяти определяемого объекта:

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

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

static Спецификатор внутреннего статического класса памяти. Применяется только(!) к именам объектов и функций. В C++ этот спецификатор имеет два значения. Первое означает, что определяемый объект располагается по фиксированному адресу. Тем самым обеспечивается существование объекта с момента его определения до конца выполнения программы. Второе значение означает локальность. Объявленный со спецификатором static локален в одном программном модуле (то есть, недоступен из других модулей многомодульной программы) или в классе. Может использоваться в объявлениях вне блоков и функций. Также используется в объявлениях, расположенных в теле функций и в блоках операторов.

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

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

6. Базовые и производные типы данных. Массивы. Указатели, ссылки и адреса. Структуры. Поля битов. Объединения

Для определения и описания переменных основных типов используются следующие базовые типы данных:

char (символьный);

short (короткий целый);

int (целый);

long (длинный целый);

float (вещественный);

double (вещественный с удвоенной точностью);

void (отсутствие значения).

Из этих базовых типов с помощью операций * & [] () и механизмов определения типов структурированных данных (классов, структур, объединений) можно конструировать множество производных типов. Обозначим именем type1 допустимый тип, приведем форматы некоторых допустимых типов:

6.1 Массивы

  •  type имя [ <константное выражение >] – массив объектов заданного типа type. Константное выражение – выражение целого типа, указывающее количество элементов массива. Например: long int[5] – массив из 5 объектов типа long int. В случае, когда массив не определяется, а описывается, список начальных значений задавать нельзя. В описании массива может отсутствовать и его размер. Например, extern unsigned long UL[] – описание внешнего массива, который определен в другой части программы, где ему выделена память и возможно начальные значения его элементам. При определении массива может выполнятся его инициализация, т.е. элементы массива получают конкретные значения. Явная инициализация элементов массива разрешена только при его определении и возможна двумя способами: либо с указанием размера массива в квадратных скобках, либо без явного указания размера, в этом случае количество элементов компилятор определяет по числу начальных значений в обязательном списке инициализации:

char CH[] = {‘A’,’B’,’C’,’D’};

int IN[7]={1,2,3}; // массив из 7 элементов, первые 3 из которых - 1,2,3

char str[]=”ABCD” // массив из 5 элементов

Имя массива является указателем-константой, значением которой служит адрес первого элемента массива (с индексом 0), т.е.

имя_массива == & имя_массива == & имя_массива[0]

6.2 Указатели, ссылки и адреса объектов

type *имя – указатель на объект типа type. Например char *ptr – указатель на объекты типа char, char *str = “stroka” – указатель на массив из 7 элементов.

type *имя[] – массив указателей на объекты типа type

type (*имя)[] – указатель на массив объектов типа type

type1 *имя (type2) – функция, принимающая аргумент типа type2 и возвращающая указатель на тип type1.

type1 (*имя) (type2) -  указатель на функцию, принимающую аргумент типа type2 и возвращающую тип type1.

type1 *(*имя)(type2) – указатель на функцию, принимающую параметр type2 и возвращающую указатель на тип type1.

type &имя = имя_объекта_типа_type – инициализированная ссылка на объект типа type.

type1 (&имя)(type2) - ссылка на функцию c параметром type2 , возвращающую значение типа type1

6.3 Структуры

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

struct { список определений; }

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

Тип_данных_структуры описатель;

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

Пример:

struct { double x,y; } s1, s2, sm[9];

struct { int year;

char moth, day; } date1, date2;

Переменные s1, s2 определяются как структуры, каждая из которых состоит из двух компонент х и у. Переменная sm определяется как массив из девяти структур. Каждая из двух переменных date1, date2 состоит из трех компонентов year, moth, day. Существует и другой способ ассоциирования имени с типом структуры, он основан на использовании тега структуры. Тег структуры аналогичен тегу перечислимого типа. Тег структуры определяется следующим образом:

struct тег { список описаний;};

где тег является идентификатором.

В приведенном ниже примере идентификатор student описывается как тег структуры:

struct student {

char name[25];

int  id, age;

 char prp;};

Тег структуры используется для последующего объявления структур данного вида в форме:

struct тег список-идентификаторов;

Пример:

struct studeut st1,st2;

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

struct node {

 int data;

 struct node * next;

} st1_node;

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

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

 st1.name="Иванов";

st2.id=st1.id;

st1_node.data=st1.age;

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

имя_указателя –> имя_элемента структуры

Также можно использовать традиционный способ доступа к переменным через указатель:

(*имя_указателя) . имя_элемента_структуры

Как и для других объектов, для структур могут быть определены ссылки:

имя_структурного_типа & имя_ссылки_на_структуру инициализатор

Например:

typedef struct {int field1; char *field2;} A;

A a,b;

A& refA = a;

A& refB(b);

6.4 Поля битов

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

struct {

идентификатор 1 : длина-поля 1;

идентификатор 2 : длина-поля  2;

}

идентификатор – один из базовых целых типов int, unsigned int (unsigned), signet int (signed), char, short, long и их знаковые и беззнаковые варианты.

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

Пример:

struct {

unsigned a1 : 1;

unsigned a2 : 2;

unsigned a3 : 5;

unsigned a4 : 2;

} prim;

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

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

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

6.5 Объединения

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

union { список описаний;};

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

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

Объединение применяется для следующих целей:

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

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

Пример:

union {

char fio[30];

char adres[80];

int vozrast;

int telefon;

} inform;

union {

int ax;

char al[2];

 } ua;

При использовании объекта inform типа union можно обрабатывать только тот элемент который получил значение, т.е. после присвоения значения элементу inform.fio, не имеет смысла обращаться к другим элементам. Объединение ua позволяет получить отдельный доступ к младшему ua.al[0] и к старшему ua.al[1] байтам двухбайтного числа ua.ax.

7. Операторы

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

  •  условные операторы, к которым относятся оператор условия if и оператор выбора switch;
  •  операторы цикла (for,while,do while);
  •  операторы перехода (break, continue, return, goto);
  •  другие операторы (оператор "выражение", пустой оператор).

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

Все операторы языка СИ, кроме составных операторов, заканчиваются точкой с запятой.

7.1 Оператор выражение

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

Примеры:

++i;

Этот оператор представляет выражение, которое увеличивает значение переменной i на единицу.

a=cos(b * 5);

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

a(x,y);

Этот оператор представляет выражение, состоящее из вызова функции.

7.2 Пустой оператор

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

  •  в операторах do, for, while, if в строках, когда место оператора не требуется, но по синтаксису требуется хотя бы один оператор;
  •  при необходимости пометить фигурную скобку.

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

Пример:

int main (){

{

if (...) goto a; /* переход на скобку */

…;

...;

…;

a:;}

return 0;

}

7.3 Составной оператор

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

{

[oбъявление]

оператор;

[оператор];

}

Заметим, что в конце составного оператора точка с запятой не ставится.

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

Пример:

int main ()

{

int q,b;

double t,d;

if (...){

int e,g;

double f,q;

}

return (0);

}

Переменные e,g,f,q будут уничтожены после выполнения составного оператора. Отметим, что переменная q является локальной в составном операторе, т.е. она никоим образом не связана с переменной q объявленной в начале функции main с типом int. Отметим также, что выражение стоящее после return может быть заключено в круглые скобки, хотя наличие последних необязательно.

7.4 Оператор if

Формат оператора:

if (выражение) оператор-1; [else оператор-2;]

Выполнение оператора if начинается с вычисления выражения.

Далее выполнение осуществляется по следующей схеме:

  •  если выражение истинно (т.е. отлично от 0), то выполняется оператор-1.
  •  если выражение ложно (т.е. равно 0),то выполняется оператор-2.
  •  если выражение ложно и отсутствует оператор-2 (в квадратные скобки заключена необязательная конструкция), то выполняется следующий за if оператор.

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

Пример:

if (i < j) i++;

else { j = i-3; i++;}

Этот пример иллюстрирует также и тот факт, что на месте оператор-1, так же как и на месте оператор-2 могут находиться сложные конструкции.

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

Примеры:

int main ( )

{

int t=2, b=7, r=3;

if (t>b)

{

if (b < r)  r=b;

}

else r=t;

return (0);

}

В результате выполнения этой программы r станет равным 2.

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

int main ( )

{

int t=2,b=7,r=3;

if ( a>b )

if ( b < c ) t=b;

else r=t;

return (0);

}

В этом случае r получит значение равное 3, так как ключевое слово else относится ко второму оператору if, который не выполняется, поскольку не выполняется условие, проверяемое в первом операторе if.

Следующий фрагмент иллюстрирует вложенные операторы if:

char ZNAC;

int x,y,z;

if (ZNAC == '-') x = y - z;

else

if (ZNAC == '+') x = y + z;

else

 if (ZNAC == '*') x = y * z;

 else

  if (ZNAC == '/') x = y / z;

  else ...

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

7.5 Оператор switch

Оператор switch предназначен для организации выбора из множества различных вариантов. Формат оператора следующий:

switch ( выражение )

{

[объявление]

[ case константное-выражение1]: [ список-операторов1]

[ case  константное-выражение2]: [ список-операторов2]

. . .

[ default: [ список операторов ]]

}

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

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

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

Все константные выражения в операторе switch должны быть уникальны. Кроме операторов, помеченных ключевым словом case, может быть, но обязательно один фрагмент помеченный ключевым словом default.

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

Отметим также, что в операторе switch можно использовать свои локальные переменные, объявления которых находятся перед первым ключевым словом case, однако в объявлениях не должна использоваться инициализация.

Схема выполнения оператора switch следующая:

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

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

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

Пример:

int i=2;

switch (i)

{

case 1: i += 2;

case 2: i *= 3;

case 0: i /= 2;

case 4: i -= 5;

 default: ;

}

Выполнение оператора switch начинается с оператора, помеченного case 2. Таким образом, переменная i получает значение, равное 6, далее выполняется оператор, помеченный ключевым словом case 0, а затем case 4, переменная i примет значение 3, а затем значение -2. Оператор, помеченный ключевым словом default, не изменяет значения переменной.

Рассмотрим ранее приведенный пример, в котором иллюстрировалось использование вложенных операторов if, переписанной теперь с использованием оператора switch.

char ZNAC;

int x,y,z;

switch (ZNAC)

{

case '+':  x = y + z; break;

case '-':  x = y - z; break;

case '*':  x = y * z; break;

case '/':  x = u / z; break;

 default: ;

}

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

Отметим, что в теле оператора switch можно использовать вложенные операторы switch, при этом в ключевых словах case можно использовать одинаковые константные выражения.

Пример:

switch (a)

{

case 1: b=c; break;

case 2:

 switch (d)

 {

  case 0: f=s; break;

  case 1: f=9; break;

  case 2: f-=9; break;

 }

case 3: b-=c; break;

}

7.6 Оператор break

Оператор break обеспечивает прекращение выполнения самого внутреннего из объединяющих его операторов switch, do, for, while. После выполнения оператора break управление передается оператору, следующему за прерванным.

7.7 Оператор for

Оператор for - это наиболее общий способ организации цикла. Он имеет следующий формат:

for ( выражение 1 ; выражение 2 ; выражение 3 ) тело

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

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

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

Схема выполнения оператора for:

  1.  Вычисляется выражение 1.
  2.  Вычисляется выражение 2.
  3.  Если значения выражения 2 отлично от нуля (истина), выполняется тело цикла, вычисляется выражение 3 и осуществляется переход к пункту 2, если выражение 2 равно нулю (ложь), то управление передается на оператор, следующий за оператором for.

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

Пример:

int main()

{

int i,b;

for (i=1; i<10; i++) b=i*i;

return 0;

}

В этом примере вычисляются квадраты чисел от 1 до 9.

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

Пример:

int main()

{

int top, bot;

char string[100], temp;

for ( top=0, bot=100 ; top < bot ; top++, bot--)

{ temp=string[top];

string[bot]=temp;

}

return 0;

}

В этом примере, реализующем запись строки символов в обратном порядке, для управления циклом используются две переменные top и bot. Отметим, что на месте выражение 1 и выражение 3 здесь используются несколько выражений, записанных через запятую, и выполняемых последовательно.

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

Пример:

for (;;)

{

...

if(…) break;

...

}

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

Пример:

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

В данном примере переменная цикла i принимает значение номера первого элемента массива t, значение которого больше 10.

7.8 Оператор while

Оператор цикла while называется циклом с предусловием и имеет следующий формат:

while (выражение) тело ;

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

Схема выполнения оператора while следующая:

  1.  Вычисляется выражение.
  2.  Если выражение ложно, то выполнение оператора while заканчивается и выполняется следующий по порядку оператор. Если выражение истинно, то выполняется тело оператора while.
  3.  Процесс повторяется с пункта 1.

Оператор цикла вида

for (выражение-1; выражение-2; выражение-3) тело ;

может быть заменен оператором while следующим образом:

выражение-1;

while (выражение-2)

{

тело

выражение-3;

}

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

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

7.9 Оператор do while

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

do тело while (выражение);

Схема выполнения оператора do while :

  1.  Выполняется тело цикла (которое может быть составным оператором).
  2.  Вычисляется выражение.
  3.  Если выражение ложно, то выполнение оператора do while заканчивается и выполняется следующий по порядку оператор. Если выражение истинно, то выполнение оператора продолжается с пункта 1.

Чтобы прервать выполнение цикла до того, как условие станет ложным, можно использовать оператор break.

Операторы while и do while могут быть вложенными.

Пример:

int i,j,k;

...

i=0; j=0; k=0;

do{

i++;

j--;

while (a[k] < i) k++;

}

while (i<30 && j<-30);

7.10 Оператор continue

Оператор continue, как и оператор break, используется только внутри операторов цикла, но в отличие от него выполнение программы продолжается не с оператора, следующего за прерванным оператором, а с начала прерванного оператора. Формат оператора следующий:

continue;

Пример:

int main()

{

int a,b;

for (a=1,b=0; a<100; b+=a,a++)

{

 if (b%2) continue;

 ... /* обработка четных сумм  */

}

return 0;

}

Когда сумма чисел от 1 до а становится нечетной, оператор continue передает управление на очередную итерацию цикла for, не выполняя операторы обработки четных сумм.

Оператор continue, как и оператор break, прерывает самый внутренний из объемлющих его циклов.

7.11 Оператор return

Оператор return завершает выполнение функции, в которой он задан, и возвращает управление в вызывающую функцию, в точку, непосредственно следующую за вызовом. Функция main передает управление операционной системе. Формат оператора:

return [выражение];

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

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

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

Пример:

int sum (int a, int b){ renurn (a+b); }

Функция sum имеет два формальных параметра a и b типа int, и возвращает значение типа int, о чем говорит описатель, стоящий перед именем функции. Возвращаемое оператором return значение равно сумме фактических параметров.

Пример:

void prov (int a, double b)

{

double c;

if (a<3) return;

else  if (b>10) return;

else {

 c=a+b;

 if ((2*c-b)==11) return;

 }

}

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

7.12 Оператор goto

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

Формат этого оператора следующий:

goto имя-метки;

...

имя-метки: оператор;

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

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

8. Функции

8.1 Определения, описания и вызовы функций

Если в таких языках, как Алгол, Фортран, ПЛ/1, Паскаль и др. делает различие между программами, подпрограммами, процедурами, функциями, то в языке Си++ и в его предшественнике - в языке Си -используются только функции.

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

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

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

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

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

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

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

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

return [выражение];

выражение в операторе return определяет возвращаемое функцией значение. Именно это значение будет результатом обращения к функции. Тип возвращаемого значения определяется типом функции. Если функция не возвращает никакого значения, т.е. имеет тип void, то выражение в операторе return опускается. В этом случае необязателен и сам оператор return в теле функции. Необходимые коды команд возврата в точку вызова компилятор языка Си++ добавит в объектный модуль функции автоматически. В теле функции может быть и несколько операторов return. Оператор return можно использовать и в функций main. Если тип возвращаемого функцией main значения отличен от void, то это значение анализируется операционной системой. Принято, что при благоприятном завершении программы возвращается значение 0. В противном случае возвращаемое значение отлично от 0.

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

Примеры определений функций с разными сигнатурами:

void print (char *name, int value) // Ничего не возвращает 

{

cout << "\n" << name << value; // Нет оператора return

}

float min(float a, float b) // У функции два оператора возврата

{

 if (а < b) return а;  //Возвращает минимальное

 return b;   // из значений аргументов

}

float cube(float x) // Возвращает значение типа float

{

return х * x * x; // Возведение в куб вещественного числа

}

int max(int n, int  m, int) // Вернет значение типа int

{

return n < m ? m : n;    

}

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

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

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

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

Приведем прототипы определенных выше функций:

void print(char *, int); // Опустили имена параметров

float min(float a, float b) ;

float cube(float x);

int max(int , int , int);

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

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

Значением выражения “вызов функции” является возвращаемое функцией значение, тип которого соответствует типу функции.

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

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

#include <имя файла>

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

#include <iostream.h>

которая из файла с именем iostream.h включает в программу описания библиотечных классов и принадлежащих им функций для ввода и вывода данных. (Из всех средств, описанных в файле iostream.h, мы до сих пор использовали только объекты потокового ввода-вывода cout, cin и соответствующие им операции >>, <<)

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

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

8.2 Начальные (умалчиваемые) значения параметров. 

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

void print(char* name = "Номер дома: ", int value = 1) { cout << "\n" << name << value; }

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

print(); // Выводит: 'Номер дома: 1'

print ("Номар комнаты: "); // Выводит: 'Номер комнаты: 1'

print (,15); // Ошибка - можно опусхать-только параметр // начиная с конца их списка

8.3 Функции с переменным количеством параметров

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

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

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

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

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

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

void va_start(va_list param, последний явный_параметр);

type va_arg(va_list param, type);

void va_end(va_list  param);

Кроме перечисленных макросов, в файле stdarg.h определен специальный тип данных va_list, соответствующий потребностям обработки переменных списков параметров. Именно такого типа должны быть первые фактические параметры, используемые при обращении к макрокомандам va_start(), va_arg(), va_end(). Объясним порядок использования перечисленных макроопределений в теле функции с переменным количеством параметров. Напомним, что каждая из функций с переменным количеством параметров должна иметь хотя бы один явно специфицированный формальный параметр, за которым после запятой стоит многоточие. В теле функции обязательно определяется объект типа va_list. Например, так:

va_list factor;

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

va_start (factor, последний явный параметр);

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

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

va_arg (factor, type)

позволяет, во-первых, получить значение очередного (вначале первого) фактического параметра типа type. Вторая задача макрокоманды va_arg() - заменить значение указателя factor на адрес следующего фактического параметра в списке. Теперь, узнав каким-то образом тип, например typel, этого следующего параметра, можно вновь обратиться к макросу:

va_arg. (factor, type1)

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

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

va_end (factor);

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

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

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

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

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

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

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

8.5 Ссылки и параметры-ссылки. 

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

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. Напомним, что использование имени функции без скобок (и без параметров) воспринимается как адрес функции.

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

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

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

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

В определении шаблона семейства функций используется служебное слово 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>

//Функция определяет ссылку на элемент с максимальным значением

template <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.  Имена параметров шаблона должны быть уникальными во всем определении шаблона.

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

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

template <class type1, class type2>

Соответственно, неверен заголовок:

template <class type1, class type2, type3>

Недопустимо использовать в заголовке шаблона параметры с одинаковыми именами.

Имя параметра шаблона (в примерах - 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 value; ... }

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

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

#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.

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

Основы ООП

9. Классы  С++

9.1 Тип данных - класс. 

Целью введения концепции классов в С++ является предоставление программисту средств создания новых типов, которые настолько же удобны в использовании, как и встроенные типы. Тип является конкретным представлением некоторой концепции. Например, встроенный тип С++ float вместе с операциями +,-,* и т.д.  является воплощением математической концепции вещественного числа. Класс- это определенный пользователем тип. Мы создаем новый тип для определения концепции, не выражаемой непосредственно встроенными типами. Например, мы могли бы ввести тип TrunkLine (междугородная линия) в программе, имеющей отношение к телефонии, тип Depositor (вкладчик) в программе управления банком или тип Predator (хищник) в программе экологического моделирования.

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

С точки зрения синтаксиса класс в С++ - это структурированный тип, образованный на основе уже существующих типов.

В этом смысле класс является расширением понятия структуры. В простейшем случае класс можно определить с помощью конструкции:

тип_класса имя_класса{список_членов_класса};

где

  •  тип_класса – одно из служебных слов class, struct, union;
  •  имя_класса – идентификатор;
  •  список_членов_класса – определения и описания типизированных данных и принадлежащих классу функций.

Функции – это методы класса, определяющие операции над объектом.

Данные – это поля объекта, образующие его структуру. Значения полей определяет состояние объекта.

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

Пример 

struct date   //дата

{int month,day,year;  // поля: месяц, день, год

void set(int,int,int);  // метод - установить дату

void get(int*,int*,int*); // метод- получить дату

void next();   // метод- установит следующую дату

void print();   //  метод - вывести дату

};

Пример

struct complex // комплексное число

{

double re,im;

double real(){return(re);}

double imag(){return(im);}

void set(double x,double y){re = x; im = y;}

void print(){cout<<”re = ”<<re; cout<<“im = ”<<im;}

};

Для описания объекта класса (экземпляра класса) используется конструкция

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

date today, my_birthday;

date *point = &today;   //указатель на объект типа date

date clim[30];    // массив объектов

date &name = my_birthday;  //ссылка на объект

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

имя_объекта.имя_класса : : имя_данного

имя_объекта.имя_класса : : имя_функции

Имя класса может быть опущено

имя_объекта.имя_данного

имя_объекта.имя_функции

Например:

класс “комплексное число”

complex x1,x2;

x1.re = 1.24;

x1.im = 2.3;

x2.set(5.1,1.7);

x1.print();

Второй способ доступа использует указатель на объект

указатель_на_объект–>имя_компонента

complex *point = &x1; // или point = new complex;

point –>re = 1.24;

point –>im = 2.3;

point –>print();

Пример.

Класс “товары”

int percent=12; // наценка

struct goods

{

char name[40];

float price;

void Input()

{

cout<<“наименование: ”;

cin>>name;

cout<<“цена: ”;

cin>>price;

}

void print()

{

cout<<“\n”<<name;

cout<<“, цена: ”;

cout<<long(price*(1.0+percent*0.01));}

};

void main(void)

{

goods wares[5];

int k = 5;

for(int i = 0; i < k; i++) wares[i].Input();

cout<<“\nСписок товаров при наценке ”<<percent<<“ % ”;

for(i = 0; i < k; i++) wares[i].print();

percent = 10;

cout<<“\nСписок товаров при наценке ”<< percent<<” % ”;

goods *pGoods = wares;

for(i = 0; i < k; i++) pGoods++–>print();

}

9.2 Доступность компонентов класса

В рассмотренных ранее примерах классов компоненты классов являются общедоступными. В любом месте программы, где “видно” определение класса, можно получить доступ к компонентам объекта класса. Тем самым не выполняется основной принцип абстракции данных – инкапсуляция (сокрытие) данных внутри объекта. Для изменения видимости компонент в определении класса можно использовать спецификаторы доступа: public, private, protected.

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

имя_объекта.имя_члена_класса;

ссылка_на_объект.имя_члена_класса;

указатель_на_объект->имя_члена_класса;

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

Защищенные (protected) компоненты доступны внутри класса и в производных классах. Защищенные компоненты нужны только в случае построения иерархии классов. Они используются также, как и private-члены, но дополнительно могут использоваться функциями- членами и функциями- “друзьями” классов, производных от описанного класса.

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

Пример 1.2.1.

class complex

{

double re, im; // private по умолчанию

public:

double real(){return re;}

double imag(){return im;}

void set(double x,double y){re = x; im = y;}

};

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

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

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

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

Пример

сomplex(double re1 = 0.0,double im1 = 0.0){re = re1; im = im1;}

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

Конструктор имеет ряд особенностей:

  •  Для конструктора не определяется тип возвращаемого значения. Даже тип void не допустим.
  •  Указатель на конструктор не может быть определен и соответственно нельзя получить адрес конструктора.
  •  Конструкторы не наследуются.
  •  Конструкторы не могут быть описаны с ключевыми словами virtual, static, const, mutuable, volatile.

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

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

имя_класса  имя_объекта(фактические_параметры);

имя_класса(фактические_параметры);

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

complex ss(5.9,0.15);

Вторая форма вызова приводит к созданию объекта без имени:

complex ss = complex(5.9,0.15);

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

имя_данного(выражение)

Пример

class A

{

int i; float e; char c;

public:

A(int ii,float ee,char cc): i(8),e( i * ee + ii ),с(сс){}

 . . .

};

Пример. Класс “символьная строка”.

#include <string.h>

#include <iostream.h>

class string

{

char *ch; // указатель на текстовую строку

int len;      // длина текстовой строки

public:

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

// создает объект – пустая строка

string(int N = 80): len(0){ch = new char[N+1]; ch[0] = ‘\0’;}

// создает объект по заданной строке

string(const char *arch){len = strlen(arch);

ch = new char[len+1];

strcpy(ch,arch);}

// компоненты-функции

// возвращает ссылку на длину строки

int& len_str(void){return len;}

// возвращает указатель на строку

char *str(void){return ch;}

. . .

};

Здесь у класса string два конструктора – перегружаемые функции.

По умолчанию создается также конструктор копирования вида

T::T(const T&),

где Т- имя класса.

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

В частности он вызывается:

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

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

string s1(“это строка”);

string s2=s1;

Здесь объект s2 инициализируется объектом s1 путем вызова конструктора копирования, созданного компилятором по умолчанию. В результате эти объекты имеют одинаковое значение в полях ch, то есть эти поля указывают на одну и ту же область памяти.

В результате при удалении объекта s1 будет освобождаться и область, занятая строкой, но она еще нужна объекту s2. Чтобы не возникало подобных ошибок, определим собственный конструктор копирования.

string(const string& st)

{len=strlen(st.len);

ch=new char[len+1];

strcpy(ch,st.ch); }

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

Например:

class complex

{double re,im;

complex(double r):re(r),im(0){ }

...

};

Этот конструктор реализует представление вещественной оси в комплексной плоскости.

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

complex b(5);

Но можно вызвать его и так

complex b=5;

Здесь необходимо преобразование скалярной величины(типа аргумента конструктора) в тип complex. Это осуществляется вызовом конструктора с одним параметром. Поэтому конструктор, имеющий один аргумент не нужно вызывать явно, а можно просто записать complex b=5  , что означает complex b = complex(3).

Преобразование, определяемое пользователем, неявно применяется в том случае, если оно уникально. Например,

class demo{

demo(char);

demo(long);

demo(char*);

demo(int*);

...}

Здесь в 

demo a=3;  

неоднозначность: вызов demo(char)? или demo(long)?

А в demo a=0; также неоднозначность: вызов demo(char*), или demo(int*), или demo(char), или demo(int) ?

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

class string{

char * ch;

int len;

public:

string(int size){

len=size; ch=new[len+1]; ch[0]=’\0’;

};

В этом случае неявное преобразование может привести к ошибке.  В случае string s=’a’; создается строка  длиной int(‘a’). Вряд ли это то, что мы хотели.

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

class string{

char * ch;

int len;

public:

explicit string(int size);

string(const char* ch);

};

string s1=’a’; // Ошибка, нет явного преобразования char в string

string s2(10); // Правильно, строка для хранения 10-ти символов –

// явный вызов конструктора

string s3=10; //Ошибка, нет явного преобразования int в string

string s4=string(10); // Правильно, конструктор вызывается явно

string s5=”строка”; // Правильно, неявный вызов конструктора //s5=string(“строка”)   

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

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

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

class demo{

int x;

public:

demo(){x=0;}

demo(int i){x=i;}

};

void main(){

class demo a[20]; //вызов конструктора без параметров(по умолчанию)

class demo b[2]={demo(10),demo(100)};//явное присваивание

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

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

~имя_класса(){операторы_тела_деструктора};

Имя деструктора совпадает с именем его класса, но предваряется символом “~” (тильда).

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

Например, при выходе за область определения или при вызове оператора delete для указателя на объект.

string *p=new string(“строка”);

delete p;

Если в классе деструктор не определен явно, то компилятор генерирует деструктор по умолчанию, который просто освобождает память занятую данными объекта. В тех случаях, когда требуется выполнить освобождение и других объектов памяти, например область, на которую указывает ch в объекте string, необходимо определить деструктор явно: ~string(){delete []ch;}

Также как и для конструктора, не может быть определен указатель на деструктор.

9.4 Компоненты-данные и компоненты-функции. Статические и константные компоненты класса

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

Компоненты-данные могут быть описаны как const. В этом случае после инициализации они не могут быть изменены.  

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

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

Вне тела класса функция определяется так

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

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

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

Например, в классе conplex:

class complex{

double re,im;

public:

//...

double real()const{return re;}

double imag()const{return im;}

};

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

Когда константная функция определяется вне класса указывать const надо обязательно:

double complex::real()const{return re:}

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

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

тип имя_класса : : имя_данного инициализатор;

Например

int goods : : percent = 12;

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

имя_объекта.имя_компонента

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

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

Однако так можно обращаться только к public компонентам.

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

имя_класса : : имя_статической_функции

Пример

#include <iostream.h>

class TPoint

{

double x,y;

static int N;  // статический компонент- данное : количество точек

public:

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

TPoint(double x1 = 0.0,double y1 = 0.0){N++; x = x1; y = y1;}

static int& count(){return N;} // статический компонент-функция

};

int TPoint : : N = 0; //инициализация статического компонента-данного

void main(void)

{

TPoint A(1.0,2.0);

TPoint B(4.0,5.0);

TPoint C(7.0,8.0);

cout<<“\nОпределены ”<<TPoint : : count()<<“точки.”;

}

10. Указатели на компоненты класса

10.1 Указатели на компоненты- данные.

Можно определить указатель на компоненты-данные.

тип_данных(имя_класса : :*имя_указателя)

В определении указателя можно включить его инициализатор

&имя_класса : : имя_компонента

Пример.

double(complex ::*pdat) = &complex :: re;

Естественно, что в этом случае данные-члены должны иметь статус открытых(pubic).

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

complex c(10.2,3.6);

c.*pdat=22.2; //изменилось значение поля re объекта c.

Указатель на компонент класса можно использовать в качестве фактического параметра при вызове функции.

Если определены указатели на объект и на компонент, то доступ к компоненту с помощью операции ‘ –>* ’.

указатель_на_объект  –>* указатель_на_компонент

Пример 

double(complex ::*pdat) = &complex :: re;

complex C(10.2,3.6);

complex *pcom = &C;

pcom –>*pdat = 22.2;

Можно определить тип указателя на компоненты-данные класса:

typedef double(complex::*PDAT);

void f(complex c, PDAT pdat) {c.*pdat=0;}

complex c;

PDAT pdat=&complex::re;

f(c,pdat);

pdat=&complex::im;

f(c,pdat);

10.2 Указатели на компоненты- функции.

Можно определить указатель на компоненты-функции.

тип_возвр_значения(имя_класса::*имя_указателя_на_функцию)(специф_параметров_функции);

Пример

// Определение указателя на функцию-член класса

double(complex ::*ptcom)();

// Настройка указателя

ptcom = &complex :: real;

// Теперь для объекта А

complex A(5.2,2.7);

// можно вызвать его функцию

cout<<(A.*ptcom)();

// Если метод real определить типа ссылки

double& real(void){return re;}

// то используя этот метод можно изменить поле re

(A.*ptcom)() = 7.9;

// При этом указатель определяется так

double&(complex : :*ptcom)();

Можно определить также тип указателя на функцию

typedef double&(complex::*PF)();

а затем определить и сам указатель

PF ptcom=&complex::real;

10.3 Указатель this

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

имя_класса *const this = адрес_объекта

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

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

void complex add(complex ob)

{this->re=this->re+ob.re;

// или *this.re=*this.re+ob.re

this->im=this->im+ob.im;}

Если функция возвращает объект, который ее вызвал, используется указатель this.

Например, пусть функция add возвращает ссылку на объект. Тогда

complex& complex add(complex& ob)

{re=re+ob.re;

im=im+ob.im;

return *this;

}

Примером широко распространенного использования this являются операции со связанными списками.

Пример. Связанный список.

#include <iostream.h>

//Определение класса

class item

{

static item *begin;

item *next;

char symbol;

public:

item (char ch){symbol = ch;} // конструктор

void add(void);  // добавить в начало

static void print(void);

};

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

void item :: add(void)

{

this –>next = begin;

begin = this;

}

void item : : print(void)

{

item *p;

p = begin;

while(p != NULL )

{

cout<<p –>symbol<<“ \t ”;

p = p –>next;

}

}

//Создание и просмотр списка

item *item : : begin = NULL; // инициализация статического компонента

void main()

{

item A(‘a’); item B(‘b’); item C(‘c’);

// включение объектов в список

A.add(); B.add(); C.add();

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

item : : print();

}

11. Друзья классов

11.1 Дружественная функция

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

Пример

class myclass

{

int x,y;

friend void set(myclass*,int,int);

public:

myclass(int x1,int y1){x = x1; y = y1;}

int sum(void){return (x+y);}

};

void set(myclass *p,int x1,int y1){p–>x = x1; p–>y = y1;}

void main(void)

{

myclass A(5,6);

myclass B(7,8);

cout<<A.sum();

cout<<B.sum();

set(&A,9,10);

set(&B,11,12);

cout<<A.sum();

cout<<B.sum();

}

Функция set описана в классе myclass как дружественная и определена как обычная глобальная функция  (вне класса, без указания его имени, без операции :: и без спецификатора friend).

Дружественная функция  при вызове не получает указатель this. Объекты класса должны передаваться дружественной функции только через параметр.

Итак, дружественная функция:

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

может быть глобальной функцией;

может быть компонентной функцией  другого ранее определенного класса.

Например,

class CLASS1

{. . .

int f(. . .);

. . .

};

class CLASS2

{. . .

friend int CLASS1 : : f(. . .);

 . . .

};

// В этом примере класс CLASS1  с помощью своей компонентной функции  f( )

// получает доступ к компонентам класса CLASS2.

может быть дружественной по отношению к нескольким классам;

Например,

// предварительное не полное определение класса

class CL2;

class CL1

{friend void f(CL1,CL2);

 . . .

};

class CL2

{friend void f(CL1,CL2);

 . . .

};

// В этом примере функция  f  имеет доступ к компонентам классов  CL1  и  CL2.

11.2 Дружественный класс

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

Например

class X2{

friend class X1;

. . .

};

class X1

{. . .

void f1(. . .);

void f2(. . .);

 . . .

};

// В этом примере функции  f1  и  f2  класса  Х1 являются друзьями класса  Х2, хотя они описываются без спецификатора friend.

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

12.1 Определение производного класса.

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

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

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

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

В иерархии производный объект наследует разрешенные для наследования компоненты всех базовых объектов (public, protected).

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

private – Член класса может использоваться только функциями- членами данного класса и функциями- “друзьями” своего класса. В производном классе он недоступен.

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

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

Следует иметь в виду, что объявление friend не является  атрибутом доступа и не наследуется.

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

class имя_класса : список_базовых_классов

{список_компонентов_класса};

В производном классе унаследованные компоненты получают статус доступа private, если новый класс определен с помощью ключевого слова class, и статус public, если с помощью struct

Например.

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

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

X,Y,Z – базовые классы.

Здесь все унаследованные компоненты классов X,Y,Z в классе A получают статус доступа private.

б) struct S : X,Y,Z{. . .};

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

X,Y,Z – базовые классы.

Здесь все унаследованные компоненты классов X,Y,Z в классе A получают статус доступа public.

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

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

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

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

public

public

protected

public

protected

protected

public

protected

protected

protected

private

public

protected

private

private

Пример

class B

{protected: int t;

public: char u;

private: int x;

};

struct S : B{   };  //  наследуемые члены t, u  имеют атрибут доступа public

class E : B{   };   // t, u  имеют атрибут доступа  private

class M : protected B{   };  // t, u – protected.

class D : public B{   };   // t – protected, u – public

class P : private B{   };   // t, u – private

Таким образом, можно только сузить область доступа, но не расширить.

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

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

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

  •  Доступ извне: доступными являются лишь элементы с атрибутом public.
  •  Собственные члены класса: доступ регулируется только атрибутом доступа, указанным при описании класса.
  •  Наследуемые члены класса: доступ определяется атрибутов доступа базового класса, ограничивается атрибутом доступа при наследовании и изменяется явным указанием атрибута доступа в производном классе.

Пример 

class Basis{

public:

int a,b;

protected:

int c;};

class Derived:public Basis{

public:

Basis::a;};

void main(){

Basis ob;

Derived od;

ob.a;  //правильно

ob.c;  //ошибка

od.a;  //правильно

od.b;  //ошибка

Доступ изнутри.

Собственные члены класса:

  •  Доступ извне возможен только для public-членов класса.
  •  private и protected-члены класса могут быть использованы только функциями- членами данного класса.

Наследуемые члены класса:

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

Пример

class Basis{

int a;

public b;

void f1(int i){a=i;b=i;}

class Derived:private Basis{

public:

void f2(int i){

a=i;  //ошибка

b=i;}  // //правильно

};

12.2 Конструкторы и деструкторы производных классов

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

Например.

class Basis

{

int a,b;

public:

Basis(int x,int y){a=x;b=y;}

};

class Inherit:public Basis

{int sum;

public:

Inherit(int x,int y, int s):Basis(x,y){sum=s;}

};

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

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

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

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

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

13. Полиморфизм

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

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

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

Пример.

class base

{

public:

void print(){cout<<“\nbase”;}

};

class dir : public base

{

public:

void print(){cout<<“\ndir”;}

};

void main()

{

base B,*bp = &B;

dir D,*dp = &D;

base *p = &D;

bp –>print(); // base

dp –>print(); // dir

p –>print(); // base

}

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

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

Пример

class base

{

public:

virtual void print(){cout<<“\nbase”;}

. . .

};

// и так далее – см. предыдущий пример.

В этом случае будет напечатано

base

dir

dir

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

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

Виртуальными могут быть только нестатические функции-члены.

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

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

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

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

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

13.2 Абстрактные классы

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

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

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

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

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

Пример

class Base{

public:

Base();                      // конструктор по умолчанию

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

virtual ~Base();     // виртуальный деструктор

virtual void Show()=0;     //  чистая виртуальная функция

// другие чистые виртуальные функции

protected:

// защищенные члены класса

private:

// часто остается пустым, иначе будет мешать будущим разработкам

};

class Derived: virtual public Base{

public:

Derived();                                // конструктор по умолчанию

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

Derived(параметры);              // конструктор с параметрами

virtual ~Derived();                   // виртуальный деструктор

void Show();                                    // переопределенная виртуальная функция

// другие переопределенные виртуальные функции

Derived& operator=(const Derived&);       // перегруженная операция присваивания

// ее смысл будет понятен после прочтения главы 3

// другие перегруженные операции

protected:

// используется вместо private, если ожидается наследование

private:

// используется для деталей реализации

};

По сравнению с обычными классами абстрактные классы пользуются “ограниченными правам”. А именно:

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

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

Пример

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

#include<iostream.h>

#include<string.h>

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

class Person

{

public:

Person()

{strcpy(name,"NONAME"); age=0; next=0;};

Person(char*NAME,int AGE)

{ strcpy(name,NAME); age=AGE; next=0;}

virtual~Person(){};

virtual void Show()=0;

virtual void Input()=0;

friend class List; //для того, чтобы в классе List было доступно поле next

protected:

char name[20]; //имя

int age; //возраст

Person* next; //указатель на следующий объект в списке

};

//Производный класс- СТУДЕНТ

class Student:public Person{

public:

Student()

{grade=0;}

Student(char* NAME,int AGE,float GRADE):Person(NAME,AGE)

{grade=GRADE; }

void Show(){cout<<"name="<<name<<"  age="<<age<<"grade="<<grade<<endl;}

void Input()

{cout<<"name=";cin>>name;

cout<<"age=";cin>>age;

cout<<"grade=";cin>>grade;}

protected:

float grade;//рейтинг

};

//Производный класс- Преподаватель

class Teacher:public Person{

public:

Teacher()

{work=0;}

Teacher(char* NAME,int AGE,int WORK):Person(NAME,AGE)

{ work=WORK;}

void Show(){cout<<"name="<<name<<"  age="<<age<<"  work="<<work<<endl;}

void Input()

{

cout<<"name=";cin>>name;

cout<<"age=";cin>>age;

cout<<"work=";cin>>work;}

protected:

int work;//стаж

};

//Класс СПИСОК

class List

{

private:

Person* begin;

public:

List(){begin=0;}

~List();

void Insert(Person*);

void Show();

};

List::~List()

{ Person*r;

while(begin!=0){ r=begin; begin=begin->next; delete r;}

}

void List::Insert(Person*p){

Person *r;

r=begin; begin=p; p->next=r;}

void List::Show()

{Person *r;

r=begin;

while(r!=0)

{r->Show(); r=r->next;}

}

void main()

{

List list;

Student* ps;

Teacher* pt;

ps=new Student("Иванов",21,50.5);

list.Insert(ps);

pt=new Teacher("Котов",34,10);

list.Insert(pt);

ps=new Student;

ps->Input();

list.Insert(ps);

pt=new Teacher;

pt->Input();

list.Insert(pt);

list.Show();

}

14. Шаблоны классов

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

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

template <слисок_ параметров_ша6лона> определекие_функции

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

Аналогично определяется шаблон семейства классов:

template <список_параметров_шаблона> определение_класса

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

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

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

Следующий шаблон позволяет автоматически формировать классы векторов с указанными свойствами:

// файл “template.vec” -  шаблон векторов

template <class Т> //Т - параметр шаблона

class Vector

{

Т *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); Проиллюстрируем сказанное следующей программой:

//формирование классов с помоцыо шаблона

#include "template .vec" // Шаблон классов "вектор"

#include <iostream.h>

main ()

{

// Создаем объект класса "целочисленный вектор" :

Vector <int> X(5);

// Создаем объект класса "символьный вектор" :

Vector <char> С (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];

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

OA1B2C3D4E

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

//Р10-12.СРР 

#include <iostream.h>

template <class  T,   int  size =  64>

class  row {   Т  *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]; }

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

00       11       24        39       4  16       5  25        б  36       7   49

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

15. Перегрузка операций

15.1 Общие сведения о перегрузке стандартных операций

В языке С++ определены множества операций над переменными стандартных типов, такие как +, *, / и т.д. Каждую операцию можно применить к операндам определенного типа.

К сожалению, лишь ограниченное число типов непосредственно поддерживается любым языком программирования. Например, С и С++ не позволяют выполнять операции с комплексными числами, матрицами, строками, множествами. Однако, все эти операции можно выполнить через классы в языке С++.

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

Пусть заданы множества А и В:

А = {а1,а2,а3};

В = {a3,a4,a5},

и мы хотим выполнить операции объединения (+) и пересечения (*) множеств.

А+В = {a1,a2,a3,a4,a5}

А*В = {a3}.

Можно определить класс Set-“множество” и определить операции над объектами этого класса, выразив их с помощью знаков операций, которые уже есть в языке С++, например, + и *. В результате операции + и * можно будет использовать как и раньше, а также снабдить их дополнительными функциями (объединения и пересечения). Как определить, какую функцию должен выполнять оператор: старую или новую? Очень просто – по типу операндов. А как быть с приоритетом операций? Сохраняется определенный ранее приоритет операций. Для распространения действия операции на новые типы данных надо определить специальную функцию, называемую “операция-функция” (operator-function).

Ее формат:

тип_возвр_значения operator знак_операции(специф_параметров) {операторы_тела_функции}

При необходимости может добавляться и прототип:

тип_возвр_значения operator знак_операции(специф_параметров)

Если принять, что конструкция operator знак_операции есть имя некоторой функции, то прототип и определение операции-функции подобны прототипу и определению обычной функции языка С++. Определенная таким образом операция называется перегруженной (overload).

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

15.2 Перегрузка унарных операций

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

Синтаксис:

в первом случае (описание в области класса):

тип_возвр_значения operator знак_операции

во втором случае (описание вне области класса):

тип_возвр_значения operator знак_операции(идентификатор_типа)

Примеры.

class person

{ int age;

. . .

public:

. . .

void operator++(){ ++age;

};

void main()

{

class person jon;

++jon;

}

class person

{

int age;

. . .

public:

. . .

friend void operator++(person&);

};

void operator++(person& ob)

{++ob.age;}

void main()

{

class person jon;

++jon;

}

15.3 Перегрузка бинарных операций

  •  Любая бинарная операция может быть определена  двумя способами: либо как компонентная функция с одним параметром, либо как глобальная (возможно дружественная) функция с двумя параметрами. В первом случае xy означает вызов x.operator(y), во втором – вызов operator(x,y).
  •  Операции, перегружаемые внутри класса, могут перегружаться только нестатическими компонентными функциями с параметрами. Вызываемый объект класса автоматически воспринимается в качестве первого операнда.
  •  Операции, перегружаемые вне области класса, должны иметь два операнда, один из которых должен иметь тип класса.

Примеры.

1)class person{…};

class adresbook

{ // содержит в качестве компонентных данных множество объектов типа       //person, представляемых как  динамический массив, список или дерево

public:

person&  operator[](int);  //доступ к i-ому объекту

};

person& operator[](int i){. . .}

void main()

{class adresbook persons;

class person record;

record = persons[3];

}

2)class person{…};

class adresbook

{ // содержит в качестве компонентных данных множество объектов типа //person, представляемых как  динамический массив, список или дерево

public:

friend person&  operator[](const adresbook&,int);  //доступ к i-ому объекту

};

person&  operator[](const adresbook& ob ,int i){. . .}

void main()

{class adresbook persons;

class person record;

record = persons[3];

}

15.4 Перегрузка операций ++ и --.

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

Префиксная форма:

operator++();

operator—();

Постфиксная форма:

operator++(int);

operator—(int);

Указание параметра int для постфиксной формы не специфицирует второй операнд, а используется только для отличия от префиксной формы.

Пример

class person

{ int age;

. . .

public:

. . .

void operator++(){ ++age;}

void operator++(int){ age++;}

};

void main()

{class person jon;

++jon;

jon++}

15.5 Перегрузка операции вызова функции

Это операция ‘()’. Она является бинарной операцией. Первым операндом обычно является объект класса, вторым – список параметров.

Пример.

class matriza // двумерный массив вещественных чисел

{

. . .

public:

. . .

double operator()(int,int); //доступ к элементам матрицы по индексам

};

double matriza::operator()(int i,int j)

{. . .}

void main()

{

class matriza a;

double k;

. . .

k:=a(5,6);

. . .

}

15.6 Перегрузка операции присваивания

Операция отличается тремя особенностями:

операция не наследуется;

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

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

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

Пользовательский класс - строка string:

class string

{

char *p;       //указатель на строку

int len;        //текущая длина строки

public:

string(char *);

~string();

void show();

};

string::string(char*ptr)

{len=strlen(ptr);

p=new chat[len+1];

if(!p){cout<<”Ошибка выделения памяти\n”);

 exit(1);}

strcpy(p,ptr);}

string::~string()

{delete[]p;}

void string::show()

{cout<<*p<<”\n”;}

void main()

string s1(“Это первая строка”),

s2(“А это вторая строка”);

s1.show; s2.show;

s2=s1;   // Это ошибка

s1.show; s2.show;

}

В чем здесь ошибка? Когда объект s1 присваивается объекту s2, указатель p объекта s2 начинает указывать на ту же самую область памяти, что и указатель p объекта s1. Таким образом, когда эти объекты удаляются, память, на которую указывает указатель p объекта s1, освобождается дважды, а память, на которую до присваивания указывал указатель p объекта s2, не освобождается вообще.

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

В этом случае необходимо самим перегружать операцию присваивания. Покажем как это сделать для нашего класса string.

class string

{

char *p; //указатель на строку

int len;  //текущая длина строки

public:

. . .

string& operator=(string& );

};

string& string::operator=(string& ob)

{

if(this==&ob) return *this;

if(len<ob.len)

{ //требуется выделить дополнительную память

delete[]p;

p=new char[ob.len+1];

if(!p){ cout<<”Ошибка выделения памяти\n”);   exit(1); }

len=ob.len;

strcpy(p,ob.p);

return *this;

}

}

В этом примере выясняется, не происходит ли самоприсваивание(типа ob=ob). Если имеет место самоприсваивание, то просто возвращается ссылка на объект.

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

Отметим две важные особенности функции operanor=.

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

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

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

15.7 Основные правила перегрузки операций. 

  •  Вводить собственные обозначения для операций, не совпадающие со стандартными операциями языка С++ , нельзя.
  •  Не все операции языка С++ могут быть перегружены. Нельзя перегрузить следующие операции:
    •  .  – прямой выбор компонента,
    •  .*  – обращение к компоненту через указатель на него,
    •  ?:  – условная операция,
    •  ::  – операция указания области видимости,
    •  sizeof’  вычисление размера
    •  , – операция последовательного вычисления,
    •  #, ## – препроцессорные операции.
  •  Каждая операция, заданная в языке, имеет определенное число операндов, свой приоритет и ассоциативность. Все эти правила, установленные для операций в языке, сохраняются и для ее перегрузки, т.е. изменить их нельзя.
  •  Любая унарная операция определяется двумя способами: либо как компонентная функция без параметров, либо как глобальная (возможно дружественная) функция с одним параметром. Выражение z означает в первом случае вызов z.operator(), во втором- вызов operator(z).
  •  Любая бинарная операция определяется также двумя способами: либо как компонентная функция с одним параметром, либо как глобальная (возможно дружественная) функция с двумя параметрами. В первом случае xy означает вызов x.operator(y), во втором – вызов operator(x,y).
  •  Перегруженная операция не может иметь аргументы (операнды), заданные по умолчанию.
  •  В языке С++ установлена идентичность некоторых операций, например, ++z – это тоже, что и z+=1. Эта идентичность теряется для перегруженных операций.
  •  Функцию operator можно вызвать по ее имени, например, z=operator*(x,y) или z=x.operator*(y). В первом случае вызывается глобальная функция, во втором – компонентная функция класса Х , и х – это объект класса Х. Однако, чаще всего функция operator вызывается косвенно, например, z=x*y
  •  За исключением  перегрузки операций new и delete функция operator должна быть либо нестатической компонентной функцией, либо иметь как минимум один аргумент (операнд) типа “класс” или “ссылка на класс” (если это глобальная функция).
  •  Операции ‘=’, ‘[]’, ‘–>’ можно перегружать только с помощью нестатической компонентной функции operator. Это гарантирует, что первыми операндами будут леводопустимые выражения.
  •  Операция ‘[]’ рассматривается как бинарная. Пусть а – объект класса А, в котором перегружена операция ‘[]’. Тогда выражение a[i] интерпретируется как a.operator[](i).
  •  Операция ‘()’ вызова функции рассматривается как бинарная. Пусть а – объект класса А, в котором перегружена операция ‘()’. Тогда выражение a(x1,x2,x3,x4) интерпретируется как a.operator()(x1,x2,x3,x4).
  •  Операция ‘–>’ доступа к компоненту класса через указатель на объект этого класса рассматривается как унарная. Пусть а – объект класса А, в котором перегружена операция ‘–>’. Тогда выражение a–>m интерпретируется как (a.operator–>())–>m. Это означает, что функция operator–>() должна возвращать указатель на класс А, или объект класса А, или ссылку на класс А.
  •  Перегрузка операций ‘++’ и ‘--‘, записываемых после операнда (z++, z--), отличается добавлением в функцию operator фиктивного параметра int, который используется только как признак отличия операций z++ и z-- от операций ++z и --z.
  •  Глобальные операции new можно перегрузить и в общем случае они могут не иметь аргументов (операндов) типа “класс”. В результате разрешается иметь несколько глобальных операций new, которые различаются путем изменения числа и (или) типов аргументов.
  •  Глобальные операции delete не могут быть перегружены. Их можно перегрузить только по отношению к классу.
  •  Заданные в самом языке глобальные операции new и delete можно изменить, т.е. заменить версию, заданную в языке по умолчанию, на свою версию.
  •  Локальные функции operator new() и operator delete() являются статическими компонентами класса, в котором они определены, независимо от того, использовался или нет спецификатор static (это, в частности, означает, что они не могут быть виртуальными).
  •  Для правильного освобождения динамической памяти под базовый и производный объекты следует использовать виртуальный деструктор.
  •  Если для класса Х операция “=” не была перегружена явно и x и y - это объекты класса Х, то выражение x=y задает по умолчанию побайтовое копирование данных объекта y в данные объекта x.
  •  Функция operator вида operator type() без возвращаемого значения, определенная в классе А, задает преобразование типа А к типу type.
  •  За исключением операции присваивания ‘=’ все операции, перегруженные в классе Х, наследуются  в любом производном классе Y.
  •  Пусть Х – базовый класс, Y – производный класс. Тогда локально перегруженная операция для класса Х может быть далее повторно перегружена в классе Y.

16. Обработка исключительных ситуаций

16.1 Операторы try, throw, catch 

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

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

В языке Си++ реализованы специальные операторы try, throw и catch предназначенные для обработки ошибочных ситуаций, которые называются исключениями.

Оператор try открывает блок кода, в котором может произойти ошибка. Если ошибка произошла, то оператор throw вызывает исключение. Исключение обрабатывается специальным обработчиком исключений. Обработчик исключения представляет собой блок кода, который начинается оператором catch.

Допустим ваше приложение должно вычислять значение выражения res = 100 / (num * (num - 7)). Если вы зададите значение переменной num, равное 0 или 7, то произойдет ошибка деления на нуль. Участок программы, в котором может случиться ошибка, объединим в блок оператора try. Вставим перед вычислением выражения проверку переменной nem на равенство нулю и семи. Если переменная num примет запрещенные значения, вызовем исключение, воспользовавшись оператором throw.

Сразу после блока try поместите обработчик исключения catch. Он будет вызываться в случае ошибки.

Программа Exception принимает от пользователя значение переменной num, а затем вычисляет выражение res = 100 / (num * (num - 7)) и отображает полученный результат на экране.

В случае, если пользователь введет число 0 или 7, тогда вызывается исключение throw. В качестве параметра оператору throw указывается переменная num. Заметим, что так как переменная num имеет тип long, считается что данное исключение также будет иметь тип long.

После вызова оператора throw управление сразу передается обработчику исключения соответствующего типа. Определенный нами обработчик отображает на экране строку "Exception, num = ", а затем выводит значение переменной num.

После обработки исключения, управление не возвращается в блок try, а передается оператору, следующему после блока catch данного обработчика исключения. Программа выведет на экран строку “Stop program” и завершит свою работу.

Если пользователь введет разрешенные значения для переменной num, тогда исключение не вызывается. Программа вычислит значение res и отобразит его на экране. В этом случае обработчик исключения не выполнится и управление перейдет на оператор, следующий за блоком обработки  исключения. Программа выведет на экран строку “Stop program” и завершит работу.

Файл Exception.cpp

#include <iostream.h>

int main()

{

long num = 7;

long res = 0;

// Введите число num

cout << "Input number: ";

 cin >> num;

// Блок try, из которого можно вызвать исключение

try {

 if((num == 7) || (num == 0))

// Если переменная num содержит значение 0 или 7,

// тогда вызываем исключение типа float

throw num;

// Значения num равные 0 или 7 вызовут ошибку

// деления на нуль в следующем выражении

res = 100 / (num * (num - 7));

// Отображаем на экране результат вычисления

 cout << "Result = " << res << endl;

}

// Обработчик исключения типа float

catch(long num)

{

 // Отображаем на экране значение переменной num

 cout << "Exception, num = " << num << endl;

}

cout << "Stop program" << endl;

return 0;

}

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

catch(long)

{

// Отображаем на экране значение переменной num

 cout << "Exception, num = " << num << endl;

}

16.2 Универсальный обработчик исключений

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

catch(...)

{

...

}

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

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

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

#include <eh.h>

#include <iostream.h>

#include <process.h>

void FastExit(void);

int main()

{

// Устанавливаем функцию term_func

set_terminate(FastExit);

try

{

 // ...

 // Вызываем исключение типа int

 throw (int) 323;

 // ...

}

 

// Определяем обработчик типа char. Обработчик исключений

// типа int и универсальный обработчик не определены

 catch(char)

{

 cout << "Exception " << endl;

}

return 0;

}

// Определение функции FastExit

void FastExit()

{

cout << "Exception handler not found" << endl;

 exit(-1);

}

Среда Visual C++  позволяет запретить или разрешить обработку исключений языка Си++. Для управления исключениями выберите из меню Build строку Settings. На экране появится диалоговая панель Project Settings, в которой определяются все режимы работы. Выберите страницу C/C++. Затем из списка Category выберите строку C++ Language. Чтобы включить обработку исключительных ситуаций установите переключатель Enable exception handling.

17. Структура Windows-приложения

17.1 Разработка Windows – приложений на языке С++

Операционные системы (ОС) MS-DOS и Windows поддерживают две совершенно разные идеологии программирования. Программа DOS после своего запуска должна быть постоянно активной. Если ей что-то требуется, к примеру, получить очередную порцию данных с устройства ввода-вывода, то она сама должна выполнить соответствующие запросы к операционной системе. При этом программа DOS работает по определенному алгоритму, она всегда знает, что и когда ей следует делать. В Windows все наоборот. Программа пассивна. После запуска она ждет, когда ей уделит внимание операционная система. ОС делает это посылкой специально оформленных групп данных, называемых сообщениями. Сообщения могут быть разного типа, они функционируют в системе достаточно хаотично, и приложение не знает, какого типа сообщение придет следующим. Отсюда следует, что логика построения Windows-приложения должна обеспечивать корректную и предсказуемую работу при поступлении сообщения любого типа. Для обеспечения нормального функционирования своей программы программист должен уметь эффективно использовать функции интерфейса прикладного программирования (Application Program Interface, API) ОС.

Windows поддерживает два типа приложений:

  •  оконное приложение – строится на базе специального набора функций API, составляющих графического пользовательского интерфейса (Graphic User Interface, GUI). Оконное приложение представляет собой программу, которая весь вывод на экран производит в графическом режиме. Первым результатом работы оконного приложения является отображение на экране специального объекта – окна. После того, как окно отображено на экране, вся работа приложения направлена на то, чтобы поддержать его в актуальном состоянии;
  •  неоконное приложение, также называемое консольным, представляет собой программу, работающую в текстовом режиме. Работа консольного приложения напоминает работу программы MS-DOS, но это лишь внешнее впечатление. Консольное приложение обеспечивается специальными функциями Windows.

Вся разница между двумя типами приложений Windows состоит в том, с каким типом информации они работают. Основной тип приложений Windows – оконные.

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

Минимальное Windows-приложение состоит из трех частей:

  •  главной функции;
  •  цикла обработки сообщений;
  •  оконной функции.

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

17.2 Структура каркасного Windows-приложения

//Простейшая программа с главным окном

//Операторы препроцессора

#define STRICT //Строгая проверка типов переменных

#include <windows.h> //Два файла с определениями, макросами и прототи-

#include <windowsx.h> //пами функций Windows

#include <string.h> //Заголовочный файл с прототипом функции memset

//Прототипы используемых в программе функций Windows

void Register(HINSTANCE); //Функция регистрации класса окна

void Create(HINSTANCE, int); //Функция создания главного (единств.) окна

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); //Оконная функция

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

char szClassName[]=”MainWindow”; //Произвольное имя класса главного окна

char szTitle[]=”Программа”; //Произвольный заголовок окна

//Главная функция WinMain

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR, int nCmdShow)

{

MSG msg; //Структура для временного хранения сообщений Windows

if (hPrevInstance==NULL) //Только для первого экземпляра приложения

Register(hInstance); // 1) Регистрация в Windows класса окна

Create(hInstance, nCmdShow); // 2) Создание и вывод на экран окна

while (GetMessage(&msg, NULL, 0, 0)) // 3) Цикл обработки сообщений:

DispatchMessage(&msg); // ждать сообщение, записать его в msg и передать WndProc

Return msg.wParam; //После выхода из цикла вернуться в Windows

}

//Функция Register() регистрации класса окна

void Register(HINSTANCE hInst)

{

WNDCLASS wc; //Структура для задания характеристик класса окна

memset(&wc, 0, sizeof(wc)); //Обнуление всех членов структуры

wc.lpszClassName=szClassName; //Записываем в структуру имя класса окна

wc.hInstance=hInst; //Дескриптор приложения

wc.lpfnWndProc=WndProc; //Определяем оконную функцию для главного окна

wc.hCursor=LoadCursor(NULL, IDC_ARROW); //Будет стандартный курсор мыши

wc.hIcon=LoadIcon(NULL, IDC_APPLICATION); //Будет стандартная пиктограмма

wc.hbrBackground=GetStockBrush(WHITE_BRUSH); //Будет белая кисть для фона окна

RegisterClass(&wc); //Вызов функции Windows регистрации класса окна

}

//Функция Create() создания и показа окна

void Create(HINSTANCE hInst, int nCmdShow)

{

HWND hwnd=CreateWindow(szClassName, szTitle, WS_OVERLAPPEDWINDOW, 10, 10, 300, 100, HWND.DESKTOP, NULL, hInst, NULL); //Класс и заголовок окна, стиль окна и его координаты и размеры, родитель, меню, дескриптор, дополнительные параметры

ShowWindow(hwnd, nCmdShow); //Вызов функции Windows показа окна

}

//Оконная функция WndProc() главного окна

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)

{

switch(msg)

{

case WM_DESTROY: //При завершении приложения пользователем

PostQuitMessage(0); //Параметр – код завершения пойдет в msg.wParam

return 0;

default: //В случае всех остальных сообщений Windows

return(DefWindowProc(hwnd, msg, wParam, lParam)); //Обработка их по умолчанию

}

}

Данная структура программы типична для простых приложений Windows. В программе описаны четыре функции: WinMain(), Register(), Create() и WndProc(). При запуске приложения Windows управление всегда передается функции WinMain(), которая должна присутствовать в любой программе. Более того, эта функция, имея в принципе циклический характер, выполняется в течение всей жизни приложения. Основное ее назначение – выполнение инициализирующих действий и организация цикла обработки сообщений, т. е. сообщить системе о новом для нее приложении, его свойствах и особенностях.

Для достижения этой цели WinMain() выполняет следующие действия:

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

Сообщения Windows являются, пожалуй, самой важной концепцией этой системы. Каждый раз, когда происходит какое-либо событие, затрагивающее интересы программы (например, пользователь выбирает пункт меню или нажимает на кнопку в окне диалога), Windows посылает приложению сообщение об этом событии. Задача функции WinMain() заключается в приеме этого сообщения и передаче его второму важнейшему компоненту любого приложения Windows – функции главного окна или оконной функции. В отличие от главной функции WinMain(), имя которой изменять нельзя, имя оконной функции может быть любым (в нашем случае – WndProc()). У внутренних окон, расположенных внутри главного окна, будут другие оконные функции.

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

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

Программа начинается с группы операторов препроцессора.

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

#include <windows.h>.

Функции Win32 API в качестве передаваемых им параметров используют множество констант и данных определенной структуры. В пакете компилятора С/С++ эти данные расположены в заголовочных файлах, совокупность которых можно рассматривать как дерево с корнем в виде файла windows.h.

Вслед за операторами препроцессора идет раздел прототипов, где определяются прототипы всех функций пользователя, включенных в программу. Прототип функции WinMain(), также как и прототипы всех остальных функций Windows (RegisterClass(), GetMessage() и другие) описан в файле windows.h и не нуждается в повторном определении.

Функции Register() и Create() придуманы для повышения структурированности программы. Типы их параметров и возвращаемых значений были определены в известной степени произвольно. Иное положение с оконной функцией WndProc(). Эта функция вызывается из Windows при поступлении в приложение того или иного сообщения. Поэтому список параметров, а также тип возвращаемого значения и прочие характеристики этой функции определены совершенно жестко и изменены быть не могут. Функция принимает четыре параметра типов: HWND, UINT, WPARAM и LPARAM и возвращает результат типа LRESULT. Кроме этого, функция должна быть объявлена описателем CALLBACK, что эквивалентно far pascal, т. е. с дальней передачей параметров.

Глобальные переменные, описываемые до главной функции WinMain(), характерны тем, что они известны, или видимы во всех функциях и других блоках программы; их объявление позволяет сократить список параметров функций. Так, переменная szClassName используется в функциях Register() и Create() без передачи ее через параметры этих функций. Однако переменная szTitle используется лишь однажды при вызове функции Windows CreateWindow() и объявлять ее глобальной нет ни какой необходимости; в список глобальных эта переменная попала лишь ради наглядности и упрощения последующей модификации программы.

17.3 Главная функция WinMain()

Запуская приложение, мы фактически передаем управление программам Windows, которые загружают в память нашу программу и вызывают из нее главную функцию приложения, которая должна иметь имя WinMain() и описатель WINAPI (эквивалентный far pascal). Вызывая функцию WinMain(), Windows передает ей четыре обусловленные параметра.

Первый параметр типа HINSTANCE, поступающий в локальную переменную hInstance, представляет собой дескриптор данного экземпляра приложения. Он назначается приложению при его запуске системой Windows и служит для его идентификации. Многие функции Windows используют этот дескриптор в качестве входного параметра, поэтому в дальнейшем мы будем сохранять его в соответствующей глобальной переменной. В данной программе сохранение hInstance не предусмотрено. Второй параметр того же типа (локальная переменная hPrevInstance) является дескриптором предыдущего экземпляра этого же приложения и имеет смысл, если приложение запускается в нескольких экземплярах. Если предыдущие экземпляры отсутствуют, т. е. приложение запущено в единственном экземпляре, этот параметр равен 0. Анализ аргумента hPrevInstance позволяет определить, является ли данный экземпляр приложения единственным. Эта методика будет описана ниже.

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

Наконец, последний параметр nCmdShow характеризует режим запуска. Режим запуска можно установить, если, создав ярлык для нашего приложения, открыть для него окно Свойства, перейти на вкладку Ярлык и раскрыть список Окно. Если далее выбрать в этом списке пункт Свернутое в значок, Windows, запуская приложение, будет свертывать его в пиктограмму. В этом случае из Windows в WinMain() поступает значение nCmdShow, равное символической константе SW_SHOWMINNOACTIVE=7. Если же включен режим Стандартный размер, окно приложения на экране развертывается до заданного в самом приложении размера, а в WinMain() поступает константа SW_SHOWNORMAL=1. Полученное значение nCmdShow используется затем в качестве параметра при вызове функции Windows ShowWindow(), хотя в ShowWindow() можно передать и любое другое допустимое значение параметра, например, SW_SHOWMAXIMIZE, если желательно развернуть окно приложения на весь экран.

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

В типичном приложении Windows главная функция WinMain() должна выполнить, по меньшей мере, три важнейших процедуры:

Зарегистрировать в Windows класс главного окна. Если помимо главного окна планируется выводить на экран внутренние, порожденные окна, то их классы тоже регистрируются. Windows выводит на экран и обслуживает только зарегистрированные окна.

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

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

Функция WinMain() после своего завершения возвращает в Windows целочисленный результат, который системой не анализируется, но может быть использован при отладке программы, поскольку интерактивный отладчик tdw.exe после завершения отлаживаемой программы выводит на экран возвращаемое функцией WinMain() значение.

17.4 Сообщения Windows

Главная функция приложения WinMain() начинается с объявления структурной переменной msg. Это важнейшая переменная, с помощью которой в программу передается содержимое сообщений Windows. Сообщение представляет собой объект особой структуры, формируемый Windows. Формирование и обеспечение доставки этого объекта в нужное место в системе позволяют управлять работой как самой Windows, так и загруженных Windows-приложений. Инициировать формирование сообщения могут несколько источников: пользователь, само приложение, ОС Windows и другие приложения. Именно наличие механизма сообщений позволяет Windows реализовать многозадачность, которая при работе на одном процессоре является, конечно, псевдомультизадачностью. Windows поддерживает очередь сообщений для каждого приложения. Запуск приложения автоматически подразумевает формирование для него собственной очереди сообщений, даже если приложение и не будет ею пользоваться. Последнее маловероятно, т. к. в этом случае у приложения не будет связи с внешним миром. Каждое сообщение представляет собой пакет из 6 данных, шаблон которых описан в файле windows.h с помощью структуры типа MSG:

typedef struct tagMSG // msg

{

HWND hwnd; // Дескриптор окна, которому адресовано сообщение

UINT message; // Код сообщения

WPARAM wParam; // Дополнительная информация (слово)

LPARAM lParam; // Дополнительная информация (двойное слово)

DWORD time; // Время отправления сообщения

POINT pt; // Позиция курсора мыши на момент отправления сообщения

} MSG; // Новое имя класса tagMSG

В этом описании тип данных POINT описан во включаемом файле windef.h и представляет собой структуру вида:

typedef struct tagPOINT

{

LONG x;

LONG y;

} POINT.

Рассмотрим подробнее поля структуры msg:

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

в поле message Windows помещает 32-битную константу – идентификатор сообщения, однозначно определяющий тип сообщения. Эти константы содержатся во включаемом файле winuser.h (включается в файле windows.h) и используются в оконной функции оператором switch для принятия решения о том, какая из его ветвей будет исполняться.

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

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

поле POINT содержит координаты курсора мыши в момент помещения сообщения в очередь.

Сообщения являются реакцией системы Windows на различные происходящие в системе события: движение мыши, нажатие клавиши, срабатывание таймера и т. д. Отличительным признаком сообщения является его код, который может принимать значения (для системных сообщений) от 1 до 0x3FF. Каждому коду соответствует своя символическая константа, имя которой достаточно ясно говорит об источнике сообщения. Так, при движении мыши возникают сообщения WM_MOUSEMOVE (код 0x200), при нажатии на левую клавишу мыши – сообщение WM_LBUTTONDOWN (код 0x201), при срабатывании таймера – WM_TIMER (код 0х113).

Перечисленные события относятся к числу аппаратных; однако сообщения могут возникать и в результате программных действий системы или прикладной программы. Так, по ходу создания и вывода на экран главного окна, Windows последовательно посылает в приложение целую группу сообщений, сигнализирующих об этапах этого процесса: WM_GETMINMAXINFO для уточнения размеров окна, WM_ERASEBKGND при заполнении окна цветом фона, WM_SIZE при оценке размеров рабочей области окна, WM_PAINT для получения от программы информации о содержимом окна и многие другие. Некоторые из этих сообщений Windows обрабатывает сама, другие обязана обработать прикладная программа.

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

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

Рассмотрим процедуру пересылки и состав сообщения на примере сообщения WM_MOUSEMOVE о движении мыши.

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

Главная функция приложения, вызывая в цикле функцию Windows GetMessage(), непрерывно опрашивает очередь сообщений. Как только в очереди обнаруживается сообщение, функция GetMessage() забирает его из очереди и переносит в структурную переменную, предназначенную для приема сообщений (у нас это переменная msg). Вызываемая далее функция DispatchMessage() вызывает оконную функцию того окна, которому предназначено данное сообщение и передает ей через ее аргументы содержимое сообщения из структуры msg. Задача оконной функции – выполнить требуемую обработку поступившего сообщения.

Содержимое сообщения WM_MOUSEMOVE выглядит следующим образом:

msg.hwnd; // Дескриптор окна под курсором мыши

msg.message; // WM_MOUSEMOVE=0x200

msg.wparam; // Комбинация битовых флагов, инициирующих нажатие клавиш

// мыши, а также клавиш CTRL и SHIFT

msg.lparam; // Позиция курсора мыши относительно рабочей области окна

msg.time; // Время отправления сообщения

msg.pt; // Позиция курсора мыши относительно границ экрана

Вызывая оконную функцию, функция DispatchMessage() передает ей 4 параметра, которые формируются на основании содержимого поступившего сообщения. В случае сообщения WM_MOUSEMOVE в аргументы функции WndProc() поступают значения msg.hwnd, msg.message, msg.wParam, msg.lParam.

Манипуляции с мышью могут порождать и другие сообщения. Так, нажатие левой клавиши возбуждает сообщение WM_LBUTTONDOWN (код 0х201), отпускание левой клавиши – сообщение WM_LBUTTONUP (код 0х202), нажатие правой клавиши – сообщение WM_RBUTTONDOWN (код 2х204). Сложнее обстоит дело с двойными щелчками клавиш: двойной щелчок левой клавиши порождает целых 4 сообщения: WM_LBUTTOMDOWN, WM_LBUTTONUP, WM_LBUTTONDBCLICK (код 0х203) и снова WM_LBUTTONUP. Программист может обрабатывать как все эти сообщения, так и только сообщения о двойном щелчке, не обращая внимание на все остальные. Механизм образования всех этих сообщений в точности такой же, как и для сообщения WM_MOUSEMOVE, даже пакеты данных для этих сообщений не различаются.

Схожим образом формируются сообщения и от клавиатуры.

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

Содержимое сообщения WM_KEYDOWN выглядит следующим образом:

msg.hwnd; // Дескриптор активного окна

msg.message; // WM_KEYDOWN=0x100

msg.wparam; // Код виртуальной клавиши

msg.lparam; // Дополнительная инф-ция (скен-код, счетчик повторения, флаг на-

// жатия ALT и т. д.)

msg.time; // Время отправления сообщения

msg.pt; // Позиция курсора мыши относительно границ экрана

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

Предусмотрены и другие сообщения, связанные с клавиатурой. Так, отпускание любой клавиши формирует сообщение WM_KEYUP (код 0х101), нажатие клавиши вместе с клавишей ALT – сообщение WM_SYSKEYDOWN (код 0х104) и т. д.

Рассмотренные сообщения относятся к сообщениям нижнего уровня – они оповещают об аппаратных событиях практически без всякой их обработки Windows. Некоторые аппаратные события предварительно обрабатываются Windows, и в приложение поступает уже результат этой обработки. Так, при нажатии левой клавиши, мыши над строкой меню аппаратное прерывание поглощается системой Windows, и вместо сообщения WM_LBUTTONDOWN формируется сообщение WM_COMMAND (код 0х111), в число параметров которого входит идентификатор того пункта меню, над которым был курсор мыши. Это избавляет нас от необходимости анализа положения курсора мыши и выделения всех положений, соответствующих прямоугольной области данного пункта меню. Другой пример – сообщение WM_NCLBUTTONDOWN (код 0хА1), которое формируется Windows, если пользователь нажал левую клавишу мыши в нерабочей области окна, т. е. на его заголовке (с целью, например, перетащить окно в другое место экрана).

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

Рассмотрим, например, сообщение WM_CREATE. Оно генерируется системой Windows в процессе создания окна, чтобы программист, перехватив это сообщение, мог выполнить необходимые инициализирующие действия: установить системный таймер, загрузить требуемые ресурсы (шрифты, растровые изображения), открыть файлы с данными и т. д. Сообщение WM_CREATE не поступает в очередь сообщений приложения и, соответственно, не изымается оттуда функцией GetMessage(). Вместо ‘того Windows непосредственно вызывает оконную функцию WndProc() и передает ей необходимые параметры. С точки зрения программиста не имеет особого значения, каким образом вызывается оконная функция – функцией DispatchMessage(), вызванной в приложении, или непосредственно программами Windows. Иногда, однако, полезно иметь в виду, что при обработке, например, сообщения WM_MOUSEMOVE, все содержимое этого сообщения находится в структурной переменной msg, а при обработке WM_CREATE мы имеем дело только с параметрами, переданными Windows в оконную функцию. В переменной msg и в это время находится старое, уже обработанное сообщение, т. е. мусор.

Аналогично, т. е. помимо очереди сообщений приложения и структурной переменной msg, обрабатываются, например, сообщения WM_INITDIALOG (инициализация диалога), WM_SYSCOMMAND (выбор пунктов системного меню), WM_DESTROY (уничтожение окна) и многие другие.

17.5 Класс окна. Регистрация и его характеристики

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

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

В первом выполнимом предложении этой функции

if (hPrevInstance==NULL) Register(hInstance);

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