2193

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

Книга

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

Общие сведения о классах С++. Организация классов потоков ввода/вывода. Встроенные, производные и пользовательские типы данных. Идентификаторы и литеральные константы. Инициализация переменных, три формы инициализации. Неинициализированный и инициализированный указатели. Использование массивов в качестве аргументов функций. Интерпретация производных типов данных на основе приоритета операторов. Преобразование значений – явный вызов конструктрора, оператор преобразования типа. Реализация перегруженных операторов с использованием дружественных функций.

Русский

2013-01-06

1.2 MB

114 чел.

МОСКОВСКИЙ ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ ЛЕСА

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

А.М. Титов

2002

СОДЕРЖАНИЕ

ВВЕДЕНИЕ 6

1. ОСНОВНЫЕ ОСОБЕННОСТИ РАЗРАБОТКИ ПРОГРАММ   НА С++ 16

1.1. Общие сведения о классах С++ 16

1.1.1. Нешаблонные классы 16

1.1.2. Шаблонные классы 21

1.2. Организация ввода/вывода данных 24

1.2.1. Организация классов потоков ввода/вывода 24

1.2.2. Форматируемый ввод/вывод 31

1.2.2.1. Форматирование с помощью набора флагов формата 31

1.2.2.2. Форматирование с помощью манипуляторов 42

2. ОСНОВНЫЕ ТИПЫ ДАННЫХ С++ И РАБОТА С ТИПАМИ ДАННЫХ 52

2.1. Лексические элементы языка 52

2.2. Идентификаторы и литеральные константы 53

2.2.1. Идентификаторы 53

2.2.2. Литеральные константы 53

2.3. Встроенные, производные и пользовательские типы данных 57

2.3.1. Встроенные типы данных 57

2.3.2. Производные типы данных 59

2.3.3. Пользовательские типы данных 60

2.4. Переменные 60

2.4.1. Определение переменной 60

2.4.2. Основные виды переменных 61

2.4.3. Инициализация переменных, три формы инициализации 62

2.5. Новые имена типов 64

2.6. Квалификатор типа typename 65

2.7. Константы 66

2.7.1. Константы определяемые директивой #define 66

2.7.2. Формальные константы 66

2.8. Перечисления 67

2.9. Указатели 69

2.9.1. Неинициализированный и инициализированный указатели 69

2.9.2. Родовой указатель и операторы преобразования типов 72

2.9.3. Создание указателя на произвольный тип 74

2.9.4. Использование указателей для представления значений 74

2.9.5. Константные указатели 76

2.9.6. Оператор преобразования константного типа 77

2.9.7. Создание указателя на указатель 79

2.9.8. Указатели на объекты классов 80

2.10. Ссылки 81

2.10.1. Определение ссылки 81

2.10.2. Реализация ссылки 82

2.10.3. Константная ссылка 83

2.10.4. Использование ссылок 84

2.10.5. Ссылка на указатель 84

2.10.6. Инициализация ссылки 85

2.10.7. Использование константных ссылок в определении функций 90

2.10.8. Ссылки на объекты классов 90

2.11. Одномерные массивы 92

2.11.1. Определение одномерного массива 92

2.11.2. Инициализация одномерного массива 93

2.11.3. Определение ссылки на массив 94

2.11.4. Использование массивов в качестве аргументов функций 94

2.11.5. Индексация массива 96

2.11.6. Динамические массивы 97

2.12. Двумерные массивы 98

2.12.1. Определение двумерного массива 98

2.12.2. Инициализация двумерного массива 99

2.12.3. Примеры с двумерными массивами 100

2.13. Строка символов 103

2.14. Указатели и массивы 104

2.14.1. Связь между указателями и массивами 104

2.14.2. Отличия между указателями и массивами 105

2.14.3. Использование итераторов для работы с массивами 106

2.15. Структуры 108

2.15.1. Определение и свойства структур 108

2.15.2. Массив структур 111

2.16. Объединения 112

2.17. Указатели на функции 114

2.17.1. Определение указателя на функцию 114

2.17.2. Массив указателей на функции 116

3. ОСНОВНЫЕ КОНСТРУКЦИИ ЯЗЫКА С++ 117

3.1. Выражения и инструкции 117

3.1.1. Выражения 117

3.1.2. Инструкции 117

3.2. Операторы и приоритеты операторов 118

3.2.1. Список операторов и их приоритеты 118

3.2.2. Интерпретация производных типов данных на основе приоритета операторов 120

3.3. Описание операторов 121

3.3.1. Оператор следования 121

3.3.2. Оператор sizeof 122

3.3.3. Оператор идентификации типов typeid 124

3.3.4. Операторы явного преобразования типов 124

3.3.5. Оператор new 126

3.3.6. Оператор delete 130

3.3.7. Условный арифметический оператор 131

3.3.8. Операторы автоувеличения и автоуменьшения 132

3.3.9. Поразрядные операторы 135

3.3.10. Логические операторы, операторы отношения и равенства 140

3.3.11. Арифметические операторы 141

3.3.12. Операторы присваивания 143

3.4. Описание инструкций 145

3.4.1. Простые и составные инструкции 145

3.4.2. Инструкции описаний имен объектов 145

3.4.3. Инструкция if 148

3.4.4. Инструкция if-else 150

3.4.5. Инструкция if-else множественного выбора 151

3.4.6. Инструкция switch 152

3.4.7. Инструкция цикла for 153

3.4.8. Инструкция цикла while 155

3.4.9. Инструкция цикла do-while 156

3.4.10. Инструкции перехода 156

4. ФУНКЦИИ ЯЗЫКА С++ 160

4.1. Определение и объявление функций 160

4.2. Прототип функции 163

4.2.1. Возвращаемое функцией значение 163

4.2.2. Передача параметров функции 167

4.3. Локальные статические переменные функции 176

4.4. Перегруженные функции 179

4.5. Рекурсивные функции 181

4.6. Встроенные функции 183

4.7. Шаблонные функции 183

4.8. Функции работы с битами 193

5. ПРОСТРАНСТВА ИМЕН 198

5.1. Определение пространства имен 198

5.2. Определение нескольких пространств имен 202

5.3. Вложенные пространства имен 203

5.4. Разделение пространства имен 205

5.5. Безымянные пространства имен 206

5.6. Заголовочные файлы стандартной библиотеки С++ 208

6. КЛАССЫ 209

6.1. Введение 209

6.2. Элементы-данные в классе, создание объекта 211

6.3. Cоздание объекта в свободной памяти 212

6.4. Создание нескольких объектов 212

6.5. Элемент-функция в классе 213

6.6. Описание элемента-функции вне класса 214

6.7. Элемент-функция, возвращающая значение 214

6.8. Элемент-функция с параметрами 215

6.9. Задание аргументов элементов-функций по умолчанию 216

6.10. Конструктор класса, первая форма инициализации элементов-данных 217

6.11. Преобразование значений – явный вызов конструктрора, оператор преобразования типа 218

6.12. Указатель this 223

6.13. Константные функции 225

6.14. Конструктор класса, вторая форма инициализации элементов-данных 227

6.15. Конструктор по умолчанию 228

6.16. Деструктор класса 230

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

6.18. Функция присваивания и оператор присваивания 238

6.19. Конструктор копирования и оператор присваивания как закрытые элементы-функции класса 246

6.20. Перегруженные функции класса 248

6.21. Статические члены класса 250

6.22. Инициализация различных типов данных, включая константы и ссылочные переменные 253

6.23. Инициализация пользовательских типов данных 255

6.24. Дружественные функции и функции - помощники класса 256

6.25. Указатели на члены класса 262

6.26. Массивы объектов 268

7. ПЕРЕГРУЖЕННЫЕ ОПЕРАТОРЫ 271

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

7.2. Унарные компонентные операторы 271

7.3. Бинарные компонентные операторы 274

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

7.5. Реализация перегруженных операторов с использованием функций - помощников класса 282

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

7.7. Оператор вызова функции 287

7.8. Оператор индексирования массива 291

7.9. Оператор доступа к члену класса 292

7.10. Операторы инкремента и декремента 293

8. ПРОИЗВОДНЫЕ КЛАССЫ 294

8.1. Основы наследования классов 294

8.2. Наследование классов с различными спецификаторами 305

8.3. Cокрытие переменных и переопределение методов. Виртуальные функции 320

8.4. Динамическое приведение типов 331

8.5. Виртуальный деструктор 334

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

8.7. Абстрактные классы 345

9. ОБРАБОТКА ИСКЛЮЧИТЕЛЬНЫХ СИТУАЦИЙ В С++ 358

9.1. Построение обработки исключительных ситуаций 358

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

9.3. Спецификация исключений 366

10. РАБОТА С ФАЙЛАМИ В С++ 370

10.1. Файловый ввод/вывод на нижнем уровне 370

10.2. Создание потоков для работы с файлами 390

10.3. Бесформатный режим ввода/вывода данных 397

10.3.1. Функции класса basic_istream 397

10.3.2. Функции класса basic_ostream 403

10.4. Форматный режим ввода/вывода данных 407

11. КОНТЕЙНЕРНЫЕ КЛАССЫ. ШАБЛОННЫЕ КЛАССЫ 412

11.1. Контейнерный класс типа вектор и итератор для этого класса 412

11.2. Наследование класса типа вектор 428

11.3. Шаблонные классы 434

11.4. Шаблонные классы вектор и итератор 440

11.5. Наследование шаблона класса типа вектор 446

СПИСОК ЛИТЕРАТУРЫ 458

ВВЕДЕНИЕ

Содержимое книги представлено в 11 главах.

Первая глава посвящена общим сведениям о классах и организации ввода/вывода в С++. На примере программы, содержащей иерархию из трех нешаблонных классов, изложены как принципы построения отдельных классов, так и основные идеи наследования классов. Класс – это определяемый пользователем новый тип данных. После определения этот тип можно использовать для создания объектов класса. Наследование классов является одним из краеугольных понятий объектно-ориентированного программирования, оно дает возможность создавать очень сложные классы, продвигаясь от общего к частному. Средства языка С++ позволяют определить параметризованные или шаблонные классы. Шаблон класса является обобщенным определением некоторого семейства классов, имеющих схожую структуру, но отличающихся типами элементов классов. Представлен пример шаблонных классов с одним параметром.

Приведенные сведения о классах используются в главе для описания организации ввода/вывода данных С++. Реализация системы ввода/вывода построена на базе иерархии классов-шаблонов. В библиотеке ввода/вывода создаются две разные версии шаблонных классов: одна - для узких 8- разрядных символов; вторая - для широких 16-разрядных символов, эта версия рассчитана для поддержки национальных алфавитов. Система ввода/вывода С++ строится на двух связанных, но различных иерархиях классов-шаблонов. Первая иерархия является производной от класса нижнего уровня basic_streambuf. Класс предназначен для буферизации данных. Буферизация используется, чтобы сделать ввод/вывод эффективным. Вторая иерархия классов, с которой чаще всего приходится иметь дело, является производной от класса basic_ios. Ввод и вывод данных может быть как бесформатным так и форматным, при этом используются перегруженные операторы << и >>. При бесформатном вводе/выводе предполагается, что форматы заданы по умолчанию. При форматном вводе/выводе предусмотрено три способа форматирования: с помощью набора флагов формата, вызова форматирующих функций-элементов и с помощью манипуляторов ввода/вывода.

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

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

Важное значение в С++ отводится указателям на объекты, которые могут определяться как неинициализированными так и инициализированными. Как для переменных и констант определены три формы инициализации указателей. Для преобразования указателей служат операторы static_cast и reinterpret_cast. Для константных указателей применяется оператор const_cast, позволяющий преобразовывать константный тип в не константный и обратно. Можно создать указатель на функцию, который должен описываться с тем же списком формальных параметров и типом возвращаемого значения.

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

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

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

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

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

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

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

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

Инструкция – это наименьшая независимая часть С++ программы. Она соответствует предложению естественного языка, инструкция завершается точкой с запятой (;). Инструкции бывают простые и составные. Простейшей формой является пустая инструкция. Составную инструкцию, содержащую определения переменных, называют блоком. Блок задает локальную область видимости в программе – идентификаторы, объявленные внутри блока, видны только в нем. К основным инструкциям языка относятся: инструкция описания имен объектов; условные инструкции if, if-else; выбирающая инструкция switch, инструкции цикла for, while, do-while; инструкции перехода break, continue, return, goto.

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

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

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

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

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

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

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

protected (защищенный) - элементы-данные и элементы-функции класса доступны для элементов- функций данного класса и классов, производных от него;

public (открытый) - элементы-данные и элементы-функции доступны для элементов-функций и других функций программы, в которой имеется представитель класса.

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

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

- находится в области видимости класса,

- вызывается для объекта данного класса (функции доступен указатель this на этот объект).

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

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

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

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

- переопределять операторы над встроенными типами (например, нельзя переопределить сложение целых чисел так, чтобы оно сопровождалось контролем переполнения);

- определять новые операторы над встроенными типами данных, например, добавить оператор сложения для двух массивов;

-  изменять предопределенный приоритет операторов.

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

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

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

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

- ссылке на объект базового класса может быть присвоена любая ссылка на объект производного класса.

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

При наличии иерархии классов имеет место сокрытие элементов-данных с одинаковыми именами, когда переменная производного класса скрывает имя переменной базового класса, также может быть сокрыта глобальная переменная с тем же именем. Кроме того, в разных классах допускается переопределение методов. Хотя принципы работы с переменными и методами в языке С++ во многом похожи, переопределение методов и сокрытие переменных при наследовании классов различаются. На сокрытую переменную можно ссылаться, приводя объект или указатель (ссылку) на объект к соответствующему типу. Вызов переопределенного метода можно осуществить динамически по фактическому типу объекта. Для этого используется механизм виртуальных методов. Виртуальный механизм не применим к объектам классов, а применяется для указателей или ссылок на классы. При этом различаются тип указателя (ссылки) на объект и тип объекта. Виртуальный механизм позволяет осуществить вызов метода не по типу указателя (ссылки), а по типу объекта, на который ссылается указатель (ссылка). Классы, в которых реализованы виртуальные функции, носят название полиморфных классов. Имеются два термина, которые часто ассоциируются с объектно-ориентированным программированием вообще и с С++ в частности. Этими терминами являются раннее связывание и позднее связывание. Раннее связывание относится к событиям, о которых можно узнать в процессе компиляции. Особенно это касается вызовов функций, которые настраиваются при компиляции. Функции раннего связывания – это “нормальные” функции, перегружаемые функции, невиртуальные функции-члены и дружественные функции класса. При компиляции функций этих типов известна вся необходимая для их вызова адресная информация. Главным преимуществом раннего связывания является то, что оно обеспечивает высокое быстродействие программ. Определение нужной версии вызываемой функции во время компиляции программы – это самый быстрый метод вызова функций. Главный недостаток – потеря гибкости. Позднее связывание относится к событиям, которые происходят в процессе выполнения программы. Вызов функции позднего связывания – это вызов, при котором адрес вызываемой функции до запуска программы неизвестен. В С++ виртуальная функция является объектом позднего связывания. Если доступ к виртуальной функции осуществляется через указатель базового класса, то в процессе работы программа должна определить, на какой тип объекта он ссылается, а затем выбрать, какую переопределяемую функцию выполнить. Главным преимуществом позднего связывания является гибкость во время работы программы. Программа может легко реагировать на случайные события. Его основным недостатком является то, что требуется больше действий для вызова функции. Это обычно делает такие вызовы медленнее, чем вызовы раннего связывания. В зависимости от нужной эффективности, следует принимать решение, когда лучше использовать раннее связывание, а когда – позднее.

В С++ реализована идентификация типов во время выполнения программы (RTTI  Run-time Type Identification), которая позволяет программе узнать истинный тип объекта, адрес которого задается с помощью указателя или ссылки базового класса. Для поддержки RTTI введены два оператора: оператор преобразования типов typeid и оператор dynamic_cast, поддерживающий преобразование типов во время выполнения программы, обеспечивая безопасное прохождение по иерархии классов. Он позволяет преобразовать указатель (ссылку) на базовый класс в указатель (ссылку) на производный класс, а также преобразовать указатель (ссылку) на производный класс в указатель (ссылку) на базовый класс, если такое преобразование корректно.

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

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

В девятой главе представлен встроенный механизм обработки ошибок, называемый обработкой исключительных ситуаций (exception handling). Благодаря этому механизму можно упростить управление и реакцию на ошибки во время выполнения программы. Обработка исключительных ситуаций в С++ осуществляется с помощью трех ключевых слов: try, catch, throw, которые используются следующим образом. Инструкции программы, во время выполнения которых мы хотим обеспечить обработку исключительных ситуаций, располагаются внутри try-блока. Если исключительная ситуация (т. е. ошибка) возникает внутри try-блока, она возбуждается (генерируется) с помощью конструкции throw. Ситуация перехватывается и обрабатывается в реакции catch. В С++ блоки try могут быть вложенными. Если в текущем блоке try нет соответствующей реакции (обработчика исключения), выбирается обработчик из ближайшего внешнего блока try. Если он не обнаружен и в этом блоке, происходит информационное завершение программы. Генерация исключения определяется конструкциями: throw выражение и throw. Выражение может вырабатывать значение: встроенных в языке типов данных, производных типов данных, пользовательских типов данных (классов). Конструкция throw без аргумента повторно устанавливает текущее исключение. Обычно оно используется, когда для дальнейшей обработки исключения необходим второй обработчик, вызываемый из первого.

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

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

Средства работы с файлами верхнего уровня основаны на создании потоков с помощью шаблонных классов basic_ifsteam, basic_ofsteam, basic_fsteam стандартной библиотеки. Классы предоставляют определенный набор конструкторов для создания файлов, а также элементы-функции открытия и закрытия файлов. Иерархия классов стандартной библиотеки позволяет осуществить набор операций с файловыми потоками, а также обеспечить контроль потоков. Данные потока ввода/вывода рассматриваются как последовательность байт. Различаются бесформатный и форматный режимы ввода/вывода данных. В бесформатном режиме на уровне ввода/вывода данные рассматриваются как последовательность байт без выделения из нее определенных элементов. При форматном режиме известна структура последовательности байт, она состоит из определенных элементов: текстовых строк, вещественных и целых значений с различным представлением, разделителей элементов, символов заполнения и др. Ввод и вывод элементов потока в форматном режиме осуществляется под управлением программных средств, которые определяют форматы представления элементов. После того как файл открыт, записать в него или сосчитать текстовые данные можно с помощью перегруженных операторов << и >> классов basic_istream и basic_ostream. Средства стандартной библиотеки поддерживают также операции со строковыми объектами на основе классов basic_istringstream, basic_ostringstream, basic_stringstream. Классы, иерархия которых начинается с класса basic_streambuf, обеспечивают создание и управление буферами потоков, обеспечивая тем самым эффективность работы системы ввода/вывода.

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

- создание массива и его инициализация списком значений;

- индексация массива с целью считывания и записи значений.

Однако имеются четыре неприятных свойства встроенного типа массив:

- размер массива должен быть постоянным выражением,

- отсутствует проверка выхода индекса за границу массива,

- нет способа узнать размер массива,

- нельзя присваивать один массив другому.

Поэтому целесообразно создать класс vect и определить в нем необходимые операции. Пример такого класса рассмотрен в 11 главе. Сначала рассмотрен класс для массива целых чисел с определенным набором конструкторов, элементов функций и перегруженных операций. Затем введен дополнительный класс-итератор vect_iterator дружественный к основному классу vect. С помощью итераторов обеспечивается последовательный доступ ко всем составным частям контейнерного класса. Рассмотрен также пример реализации класса vect, в котором содержатся функции итератора и нет необходимости использовать дополнительный класс. Механизм наследования позволяет создать производный от vect класс d_vect, в котором осуществляется контроль выхода индекса за границы массива. Для этого переопределяется операторная функция индексирования массива, которая является виртуальной. При выходе за границы массива в этой функции для производного класса возбуждается исключительная ситуация.

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

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

Большинство примеров подготовлены в среде С++Builder.


1. ОСНОВНЫЕ ОСОБЕННОСТИ РАЗРАБОТКИ ПРОГРАММ   НА С++

1.1. Общие сведения о классах С++

1.1.1. Нешаблонные классы

Чтобы понять основные принципы программирования на С++, рассмотрим программу, содержащую иерархию из трех классов: базового класса Color_subject, производного от него класса Circle и класса Cylinder, производного от класса Circle.

Пример 1.1.1

#include <iostream>                          // директива препроцессора

using namespace std;                         // usingдиректива

const double pi = 3.141592653589793;         // вещественная константа

class Color_subject                          // базовый класс

 {public:                                   // открытые элементы

     enum Tcolor                            // перечисление

         {white, blue,    green, cyan,  yellow,

          red,   magenta, brown, black, gray};

     Color_subject(Tcolor c)                // конструктор класса

         {color = c; num_subj++;}

     // Элементы-функции

     static int get_num_subj()           // статическая функция

         {return num_subj;}              // не может быть константной

     Tcolor get_color() const {return color;}

     void put_color(Tcolor c) {color = c;}

  private:                                  // закрытые элементы

     static int num_subj;                   // статическая переменная

     Tcolor color;                          // элемент-данное

 };

int Color_subject::num_subj = 0;  // инициализация статической переменной

class Circle: public Color_subject               // производный класс

 {public:

     Circle(Tcolor color, int r)      // конструктор

         :Color_subject(color)      // вызов конструктора базового класса

              {radius = r;}

     int get_radius() const {return radius;}

     void put_radius(int r) {radius = r;}

     double circumference() const

           {return 2*pi* radius;}

     double area_circle() const

           {return pi*radius*radius;}

  private:

     int radius;

 };

class Cylinder: public Circle                 // производный класс

 {public:

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

     Cylinder(Tcolor color, int radius, int h)

         :Circle(color, radius)   // вызов конструктора базового класса

        {height = h;}

     int get_height() const {return height;}

     void put_height(int h) {height = h;}

     double area_cylinder() const;           // прототип функции

     double volume_cylinder() const

           {return area_circle()*height;}

  private:

     int height;

 };

double Cylinder::area_cylinder() const   // описание функции вне класса

     {return 2*area_circle() + 2*pi*get_radius()*height;}

int main()

  {Color_subject obj_color =

                         Color_subject(Color_subject:: white);

   cout << "sizeof(obj_color) = "<< sizeof(obj_color)<< endl;

   Circle obj_Circle = Circle(Color_subject::red, 5);

   cout << "color = " << obj_Circle.get_color() << "  "

        << "circumference = " << obj_Circle.circumference()

        << "  " << "area_circle = " << obj_Circle.area_circle()

        << endl;

   cout << "sizeof(obj_Circle) = "<< sizeof(obj_Circle)<< endl;

   Cylinder obj_Cylinder = Cylinder(Color_subject::green,5,10);

   cout << "color = " << obj_Cylinder.get_color() << "  "

        << "area_cylinder = " << obj_Cylinder.area_cylinder()

        << "  " << "volume_cylinder = "

        << obj_Cylinder.volume_cylinder() << endl;

   cout << "sizeof(obj_Cylinder)= " << sizeof(obj_Cylinder)

        << endl;

   cout << "num_subj = " << Color_subject::get_num_subj()

        << endl;

   return 0;}

Результаты:

  sizeof(obj_color) = 1

  color = 5  circumference = 31.4159  area_circle = 78.5398

  sizeof(obj_Circle) = 8

  color = 2  area_cylinder = 471.239  volume_cylinder = 785.398

  sizeof(obj_Cylinder)= 12

  num_subj = 3

1.1.1.1. Программа начинается с директивы препроцессора #include, подключающей стандартный заголовочный файл iostream библиотеки ввода/вывода. В частности, эта библиотека содержит информацию о потоках ввода/вывода cout и cin. Для нового стандарта языка С++, названного Standard C++, заголовочные файлы, вводимые директивой #include, не являются именами файлов и для них не надо указывать расширение .h, а следует указывать только имя заголовка в угловых скобках.

1.1.1.2. Непосредственно за директивой препроцессора следует usingдиректива пространства имен std. Стандартная библиотека определена в пространстве имен std. При отсутствии usingдирективы пришлось бы вместо cout, используемого в примере, писать std::cout. Наличие usingдирективы позволяет записывать идентификаторы, определенные в пространстве имен, без указания пространства имен.

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

1.1.1.4. Символ // (двойная косая черта) используется в С++ для однострочного комментария. Многострочный комментарий начинается с символов /* и заканчивается символами */.

1.1.1.5. В рассматриваемом примере после директив вводится константа pi вещественного типа (квалификатор константы const, спецификатор типа – double), инициируемая значением 3.141592653589793.

1.1.1.6. За объявлением константы идут объявления классов: Color_subject, Circle, Cylinder. Класс (class) – это определяемый пользователем новый тип данных. После определения новый тип можно использовать для создания объектов (objects). Важно понимать, что описание класса создает только логическую конструкцию, определяющую форму и природу объекта, а не фактический объект. Фактический объект или экземпляр класса создается отдельно. Описание класса содержит:

- служебное слово class;

- имя класса;

- конструкцию наследования (если класс порожден от базового класса) вида

: public Color_subject;

- тело класса, заключенное в фигурные скобки и заканчивающееся символом ‘;’.

Тело класса содержит элементы или члены класса, которые делятся на две основные категории:

- данные, называемые элементами-данными (data-members);

- функции, называемые элементами-функциями (member-functions) или методами.

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

Примерами выражений являются:

radius = r                 // выражение присваивания

2*pi*radius                // выражение умножения

Из них образуются инструкции:

    radius = r;

    return 2*pi*radius;

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

- типа возвращаемого значения;

- имени функции;

- списка параметров, заключенных в круглые скобки;

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

Первые три части составляют прототип (prototype) функции. Прототип функции заканчивается ‘;’.

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

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

int get_color() const {return color;}

void put_color(int c) {color = c;}

static int get_num_subj() {return num_subj;}

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

Функция get_num_subj() является статической функцией класса. Вызов функции осуществляется с использованием имени класса: Color_subject::get_num_subj().

Функция может быть полностью определена в классе, можно объявить в классе только прототип функции, а определение функции дать вне класса. В качестве примера может служить функция area_cylinder() класса Cylinder.

1.1.1.11. Наследование (inheritance) является одним из краеугольных понятий объектно-ориентированного программирования, потому что оно позволяет создавать очень сложные классы, продвигаясь от общего к частному. Используя наследование, можно создать главный класс, который определяет свойства, общие для набора связанных элементов. Затем этот класс может быть унаследован другими, более специфическими классами, каждый из которых добавляет те свойства, которые являются уникальными для него. Класс, от которого порождается другой класс, называется базовым (base class). Порожденный от базового класс называется производным классом (derived class).

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

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

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

:Color_subject(color)

:Circle(color, radius)

означают соответственно:

- вызов конструктора базового класса  Color_subject из производного класса Circle с фактическим параметром color;

- вызов конструктора базового класса Circle из производного класса Cylinder с фактическими параметрами color, radius.

Конструктор класса Cylinder вызывается из функции main().

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

В рассматриваемом примере в функции main() с помощью конструкторов создаются три объекта, объект класса Color_subject, окружность и цилиндр:

Color_subject obj_color = Color_subject(Color_subject:: white);

Сircle obj_Circle            = Circle(Color_subject::red, 5);

Cylinder obj_Cylinder   = Cylinder(Color_subject::green, 5, 10);

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

obj_Circle.get_color()

obj_Circle.circumference()

obj_Circle.area_circle()

obj_Cylinder.get_color()

obj_Cylinder.area_cylinder

obj_Cylinder.volume_cylinder()

Вызов статической функции get_num_subj() класса Color_subject осуществляется с помощью конструкции: Color_subject::get_num_subj().

С помощью оператора sizeof() определяется размер объекта в байтах.

Организация ввода/вывода данных изложена в разделе 1.2.

1.1.2. Шаблонные классы

1.1.2.1. В рассматриваемом примере переменная radius класса Circle и переменная height класса Cylinder принадлежали к целому типу. Очевидно, что эти переменные могут иметь вещественный тип. Средства языка С++ позволяют определять так называемые шаблонные классы (template classes), в которых типы переменных являются формальными параметрами, например,

template <class Type>               // тип Type является параметром класса

//template <typename Type>          // альтернативное задание типа Type 

class Cylinder: public Circle<Type> // Circle<Type> имя шаблонного класса

 {private:

     Type height;

  /* … */

 };

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

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

Circle<Type>, Cylinder<Type>

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

Создание класса из шаблонного класса называется конкретизацией.

1.1.2.2. Изменим предыдущий пример таким образом, чтобы классы Circle и Cylinder были шаблонными классами.

Пример 1.1.2

#include <iostream>

using namespace std;

const double pi = 3.141592653589793;         // вещественная константа

class Color_subject                          // базовый класс

 {public:                                   // открытые элементы

     enum Tcolor                            // перечисление

         {white, blue, green, cyan,  yellow,

          red,   magenta, brown, black, gray};

     Color_subject(Tcolor c)                // конструктор класса

                  {color = c; num_subj++;}

     static int get_num_subj()              // элементы функции

         {return num_subj;}

     Tcolor get_color() const {return color;}

     void put_color(Tcolor c) {color = c;}

  private:                                  // закрытые элементы

     static int num_subj;                   // статическая переменная

     Tcolor color;                          // элемент-данное

 };

int Color_subject::num_subj = 0;

template <class Type>

class Circle: public Color_subject

 {public:

     Circle(Tcolor color = black,

                Type r = Type(5)):Color_subject(color)

            {radius = r;}

     Type get_radius() const {return radius;}

     void put_radius(Type r) {radius = r;}

     double circumference() const

           {return 2*pi*radius;}

     double area_circle() const

           {return pi*radius*radius;}

  private:

     Type radius;};

template <class Type>

class Cylinder: public Circle<Type>

 {public:

   Cylinder(Tcolor color = black,

          Type radius = Type(5), Type h = Type(10)):

                                    Circle<Type>(color, radius)

            {height = h;}

   Type get_height() const {return height;}

   void put_height(Type h) {height = h;}

   double area_cylinder() const;              // прототип функции

   double volume_cylinder() const

           {return area_circle()*height;}

  private:

   Type height;};

template <class Type>                    // описание функции вне класса

double Cylinder<Type>::area_cylinder() const

     {return 2*area_circle() + 2*pi*get_radius()*height;}

int main()

 {Color_subject obj_color =

                         Color_subject(Color_subject:: white);

  cout << "sizeof(obj_color) = "<< sizeof(obj_color)<< endl;

  Circle<int> obj_Circle1 = Circle<int>(Color_subject::red, 5);

  cout << "color = " << obj_Circle1. get_color() << "  "

       << "circumference = " << obj_Circle1.circumference()

       << "  " << "area_circle = " << obj_Circle1.area_circle()

       << endl;

  Circle<double> obj_Circle2 =

                    Circle<double>(Color_subject::red, 5.5);

  cout << "color = " << obj_Circle2. get_color() << "  "

       << "circumference = " << obj_Circle2.circumference()

       << "  " << "area_circle = " << obj_Circle2.area_circle()

       << endl;

  Cylinder<int> obj_Cylinder1 =

                    Cylinder<int>(Color_subject::green, 5, 10);

  cout << "color = " << obj_Cylinder1. get_color() << "  "

       << "area_cylinder = " << obj_Cylinder1.area_cylinder()

       << "  " << "volume_cylinder = "

       << obj_Cylinder1.volume_cylinder() << endl;

  Cylinder<double> obj_Cylinder2 =

             Cylinder<double>(Color_subject::green, 5.5, 10.5);

  cout << "color = " << obj_Cylinder2. get_color() << "  "

       << "area_cylinder = " << obj_Cylinder2.area_cylinder()

       << "  " << "volume_cylinder = "

       << obj_Cylinder2.volume_cylinder() << endl;

  cout << "num_subj = " << Color_subject:: get_num_subj()

       << endl;

  return 0;}

Результаты:

  sizeof(obj_color) = 1

  color = 5  circumference = 31.4159  area_circle = 78.5398

  color = 5  circumference = 34.5575  area_circle = 95.0332

  color = 2  area_cylinder = 471.239  volume_cylinder = 785.398

  color = 2  area_cylinder = 519.934  volume_cylinder = 997.848

  num_subj = 5

1.1.2.3. В конструкторах классов Circle и Cylinder введены значения формальных параметров по умолчанию. Например, описание первого параметра

Tcolor color = black

означает, что формальный параметр color в пределах иерархии классов имеет тип Tcolor и его значение по умолчанию равно black. Описание второго параметра

Type radius = Type(5)

означает, что по умолчанию значение радиуса инициализируется значением 5, которое приводится к типу Type (поэтому используется запись Type(5)).

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

1.1.2.4. Описание функции вне шаблонного класса (как и самого шаблонного класса) начинается с ключевого слова template, за которым следует заключенный в угловые скобки список параметров шаблона, разделенных запятыми. В качестве имени функции используется квалифицированное имя, включающее квалификатор (Circle <Type>::, Cylinder<Type>::) и имя функции. Например, имеем:

template <class Type>

double Cylinder<Type>::area_cylinder() const {/* . . . */}

1.1.2.5. В функции main() осуществляется конкретизация шаблонных классов. С помощью конструкторов создаются по два объекта для окружности и цилиндра с типами int и double:

  Circle<int> obj_Circle1 = Circle<int>(Color_subject::red, 5);

  Circle<double> obj_Circle2 =

                    Circle<double>(Color_subject::red, 5.5);

  Cylinder<int> obj_Cylinder1 =

                    Cylinder<int>(Color_subject::green, 5, 10);

  Cylinder<double> obj_Cylinder2 =

             Cylinder<double>(Color_subject::green, 5.5, 10.5);

Здесь введены новые имена типов: Ti1, Td1, Ti2, Td2 с помощью ключевого слова (спецификатора объявления) typedef.

1.2. Организация ввода/вывода данных

1.2.1. Организация классов потоков ввода/вывода

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

Реализация системы ввода/вывода для нового стандарта языка С++, названного Standard C++, построена на базе системы классов-шаблонов (template classes). В классе-шаблоне определяется только форма класса без полного задания данных, с которыми он работает. После того как класс-шаблон определен, появляется возможность создавать отдельные экземпляры этого класса. Для нового стандарта языка С++ в библиотеке ввода/вывода создаются две разные версии классов-шаблонов ввода/вывода:

  1. одна - для узких (tiny) 8-разрядных символов;
  2. другая - для широких (wide) 16 разрядных символов, эта версия рассчитана для поддержки национальных алфавитов.

В дальнейшем мы будем рассматривать только классы потоков с 8-разрядными символами.

Система ввода/вывода С++ строится на двух связанных, но различных иерархиях классов-шаблонов.

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

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

typedef basic_streambuf <char> streambuf;

Производными от класса basic_streambuf являются классы basic_filebuf и basic_stringbuf. Классы filebuf и stringbuf получаются из этих классов:

typedef basic_filebuf <char> filebuf;

typedef basic_stringbuf <char> stringbuf;

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

basic_istream, basic_ostream, basic_iostream, basic_ifstream, basic_ofstream, basic_fstream, basic_istringstream, basic_ostringstream, basic_stringstream

Как уже отмечалось, библиотека ввода/вывода создает две отдельные версии иерархий классов: одну для узких 8-разрядных символов и другую для широких 16-разрядных символов. В представленной ниже табл. 1.2.1 показано соответствие имен классов-шаблонов их версиям для 8-разрядных символов.

Таблица 1.2.1

Класс-шаблон

Класс для 8-разрядных символов

basic_streambuf

basic_filebuf

basic_stringbuf

basic_ios

basic_istream

basic_ostream

basic_iostream

basic_ifstream

basic_ofstream

basic_fstream

basic_istringstream

basic_ostringstsream

basic_stringstream

Классы istream и ostream получаются из соответствующих классов basic_istream и basic_ostream следующим образом:

typedef basic_istream<char>  istream;

typedef basic_ostream<char>  ostream;

Аналогично получаются и другие классы.

Иерархия классов для 8-разрядных символов показана на рис. 1.2.1.

ios_base

ios<>

istream<>                                      ostream<>

istringstream<>         ifstream<>         iostream<>         ofstream<>         ostringstream<>

fstream<>                   stringstream<>

streambuf<>

filebuf<>                                       stringbuf<>

Рис. 1.2.1.

  1.  ios_base независимый от 8 и 16 разрядных символов базовый класс иерархии;
  2. классы с суффиксом <> порождены от шаблонных классов, имена которых начинаются с basic_;
  3. пунктирная линия означает виртуальные базовые классы (к ним относятся istream и ostream).

Дадим пояснения к классам:

  1. класс iosбазовый класс потоков ввода/вывода;
  2. класс istream – производный от ios класса, обеспечивает работу потока ввода;
  3. класс ostream – производный от ios класса, обеспечивает работу потока  вывода;
  4. класс iostream - производный от  istream и ostream  поддерживает двунаправленный ввод/вывод;
  5. класс ifstream производный от istream связывает ввод программы с файлом;
  6. класс ofstream производный от ostream связывает вывод программы с файлом;
  7. класс fstream - производный от iostream связывает как ввод так и вывод с файлом;
  8. класс istringstream производный от istream позволяет считывать из строки string;
  9. класс ostringstream производный от ostream позволяет записывать в строку string;
  10. класс stringstream производный от iostream позволяет считывать и записывать в строку string;
  11. класс streambuf предназначен для создания и управления буферами потоков ввода/вывода;
  12. класс filebuf производный от streambuf предназначен для создания и управления буферами потоков, присоединенными к файлам;
  13. класс stringbuf производный от streambuf предназначен для создания и управления буферами потоков, присоединенными к некоторым областям памяти, каждую область можно трактовать как символьный массив.

Классы, иерархия которых начинается с класса basic_ios, для эффективной организации ввода/вывода используют классы, обеспечивающие буферизацию данных (с иерархией, начинающейся с класса streambuf). Рассмотрим, например, взаимодействие между классами ostream и streambuf (рис. 1.2.2) при записи информации в приемное устройство (экран монитора, файл, принтер и др.).

tellp()

begin

current

end

Реальный приемник

 

ostream                   streambuf

Буфер символов

Рис. 1.2.2.

Здесь

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

- begin – указатель начала буфера символов;

- current – указатель текущей позиции буфера;

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

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

basic_ostream& flush();

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

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

iostream 

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

  1.  iomanip (параметризованные манипуляторы),
  2.  fstream (файловый ввод/вывод),
  3.  sstream (считывание и запись в строку string),
  4.  streambuf (работа с буферами потоков).

Следует отметить, что fstream уже включает iostream, так что включать оба элемента не обязательно.

Из иерархии классов на рис. 1.2.1 ограничимся сначала рассмотрением только четырех классов:

ios_base, ios, istream, ostream

Для удобства организации ввода/вывода в библиотеке определены четыре стандартных объекта потоков:

1. cin – объект класса istream, соответствующий стандартному вводу. Обычно он позволяет читать данные с терминала пользователя.

2. cout – объект класса ostream, соответствующий стандартному выводу. Обычно он позволяет выводить данные на терминал пользователя.

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

4. clog – объект класса ostream, соответствующий стандартному выводу c буферизацией для ошибок. В этот поток направляются сообщения об ошибках программы.

Определение объектов стандартных потоков в библиотеке ввода/вывода имеет вид:

istream  cin;

ostream cout;

ostream cerr;

ostream clog;

Вывод осуществляется, как правило, с помощью перегруженного оператора сдвига влево (<<), определенного в шаблонном классе basic_ostream, а ввод – с помощью оператора сдвига вправо (>>), определенного в шаблонном классе basic_istream.

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

<< x

перемещает данные из x, а

>> x

перемещает данные в x.

1.2.1.2. Приведем примеры. Рассмотрим простейшую программу перевода дюймов (inches) в сантиметры.

Пример 1.2.1

#include <iostream>  // стандартный заголовочный файл библиотеки ввода/вывода

using namespace std; // директива доступа ко всем идентификаторам пространства

// имен стандартной библиотеки std

int main()                 // функция main() без параметров

  {// Последовательность инструкций функции main()

   int inch = 0;         // определение и инициализация переменной inch  

   cout << "inches = ";  // вывод на экран текста: inches =

   cin >> inch;          // ввод с экрана текста целого значения в inch

   cout << inch;         // вывод на экран значения переменной inch

   cout << " in = ";     // вывод на экран текста: in =

   cout << inch*2.54;    // вывод на экран значения в сантиметрах 

   cout << " cm\n";      // вывод на экран текста: cm

                          // запись \n означает переход на другую строку

   }

Результаты: inches = 12

12 in = 30.48 cm

Отметим, что изображение вида "inches = " в выражении

cout << "inches = "

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

" cm\n" означает символ перевода строки, наличие специального символа \t означает табуляцию. Отдельные символы при изображении заключаются в одиночные кавычки: 'E', 'n', 'd' и т.п.

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

Пример 1.2.2

#include <iostream>

using namespace std;

int main()

  {int inch = 0;

   cout << "inches = \t";

   cin >> inch;

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

   cout << 'E' << 'n' << 'd' << endl;

   return 0;}

Результаты:

inches =        12

12 in =         30.48 cm

End

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

Пример 1.2.3

#include <iostream>  // стандартный заголовочный файл библиотеки ввода/вывода

using namespace std; // директива доступа ко всем идентификаторам пространства

// имен стандартной библиотеки std

int main()           // функция main() без параметров

  {// Последовательность инструкций функции main()

   double x;   // определение переменной вещественного типа без инициализации

   int i;      // определение переменной целого типа без инициализации

   cout << "Input a double: ";             // вывод текста

   cin >> x;                // ввод значения переменной вещественного типа

   cout << "Input a positive integer: ";   // вывод текста

   cin >> i;                // ввод значения переменной целого типа

   if (i < 10)              // инструкция if 

      cerr << "error i = " << i << endl;  // вывод текста и значения

   cout << "i * x = " << i * x << endl;   // вывод результата

   return 0;}

Результаты:

Input a double: 3.14

Input a positive integer: 4

error i = 4

i * x =  12.56

Пример 1.2.4

#include <iostream>

Чтобы обеспечить эти особенности, осуществляются перегрузки оператора (overloaded operator) ввода >> и оператора вывода << для различных типов данных. Перегрузка оператор ввода >> дана в шаблонном классе basic_istream, оператора вывода << дается в шаблонном классе basic_ostream.

1.2.1.3. Каждый перегруженный оператор возвращает значение класса basic_istream или класса basic_ostream по ссылке, это позволяет повторно применять оператор >> или оператор << к предыдущему значению, и дает возможность в одной инструкции ввода/вывода использовать несколько значений. Дадим пояснения. Для этого рассмотрим инструкцию вида:

    cout << inch*2.54;

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

cout.operator << (inch*2.54);

Так как результатом функции basic_ostream:: operator <<() является объект типа basic_ostream, а именно используется поток (в нашем случае cout), то к нему опять можно применить оператор <<, например

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

, что интерпретируется как

(cout.operator << (inch*2.54)).operator << (" cm\n");

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

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

Пример 1.2.5

#include <iostream>

using namespace std;

void readIn()  {cout << "I am function readIn()\n";}

void sort()    {cout << "I am function sort()\n";}

void compact() {cout << "I am function compact()\n";}

void print()   {cout << "I am function print()\n";}

int main()

  {readIn();

   sort();

   compact();

   print();

   return 0;}

Результаты:

    I am function readIn()

    I am function sort()

    I am function compact()

    I am function print()

1.2.2. Форматируемый ввод/вывод 

До сих пор во всех примерах для ввода/вывода информации на экран использовались форматы, заданные в С++ по умолчанию. Однако информацию можно выводить в широком диапазоне форм. При этом с помощью системы ввода/вывода С++ можно форматировать данные. Кроме того, можно изменять определенные параметры ввода информации.

Библиотека ввода/вывода предусматривает три способа форматирования: с помощью набора флагов формата (format flags), вызова форматирующих функций-элементов и с помощью манипуляторов ввода/вывода (Input/Output manipulators).

1.2.2.1. Форматирование с помощью набора флагов формата

class ios_base {

 public:

  /* . . . */

  typedef int      fmtflags;     // тип флагов форматирования

 // битовые маски форматирования

  enum fmt_flags

       {boolalpha    = 0x0001,

        dec               = 0x0002,

        fixed             = 0x0004,

        hex               = 0x0008,

        internal        = 0x0010,

        left                = 0x0020,

        oct                 = 0x0040,

        right           = 0x0080,

        scientific       = 0x0100,

        showbase     = 0x0200,

        showpoint    = 0x0400,

        showpos       = 0x0800,

        skipws         = 0x1000,

        unitbuf         = 0x2000,

        uppercase    = 0x4000,

        adjustfield   = left | right | internal,

        basefield      = dec | oct | hex,

        floatfield      = scientific | fixed};

  /* . . . */

  // форматирующие функции

  fmtflags flags() const;                                      // считывание флагов

  fmtflags flags(fmtflags fmtfl);                         // установка флагов 

  fmtflags setf(fmtflags fmtfl);                           // добавление флагов с одним аргументом

  fmtflags setf(fmtflags fmtfl, fmtflags mask); // добавление флагов с двумя аргументами

  void unsetf(fmtflags mask);                             // сброс флагов

  streamsize precision() const;                           // считывание точности

  streamsize precision(streamsize prec);          // установка точности

  streamsize width() const;                                // считывание ширины поля  

  streamsize width(streamsize wide);               // установка ширины поля

  /* . . . */

};   // конец класса

Таблица 1.2.2

Если установлен флаг, булевы значения выводятся как

слова “true”,”false”. В противном случае они

Устанавливается десятичное представление чисел.

Если установлен флаг, вещественные числа выводятся в

Если установлен флаг, при вводе чисел знак или индикатор

основания  выводятся на  левом краю поля  вывода, а само

число выравнивается по правому краю поля. Промежуток

Если установлен флаг, данные выравниваются по левому

краю поля вывода не сбрасываются

Устанавливается восьмеричное представление чисел.

Если установлен флаг, данные выравниваются по правому

краю поля вывода. Флаг установлен по умолчанию

Если установлен флаг, вещественные числа выводятся в

научной (экспоненциальной) нотации

Если установлен флаг, то при восьмеричном и

шестнадцатеричном представлениях чисел выводится

индикатор основания (0 – для восьмеричных и 0x – для

шестнадцатеричных чисел)

Если установлен флаг, то для вещественных чисел всегда

выводится десятичная точка.

Если установлен флаг, то выводится знак + для

положительных чисел

Если установлен флаг, при вводе игнорируются начальные

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

Когда флаг сброшен пробельные символы   не игнорируются

Используется для очистки буфера после каждой операции

вывода

Если установлен флаг, то шестнадцатеричные цифры от А до F, а также символ // экспоненты E выводятся в   верхнем регистре.

Флаги форматов удобно изобразить в виде битовой шкалы, наличие 1 в соответствующем разряде означает, что соответствующий флаг установлен, наличие 0 означает отсутствие установки флага (рис. 1.2.3).

Флаги adjustfield, basefield, floatfield являются составными. Виды этих флагов приведены на рис 1.2.4.

ios_base::boolalpha,  ios_base::dec, ios_base::hex, ios_base::oct,

ios_base::scientific, ios_base::fixed, ios_base::showbase, ios_base::showpoint, 

ios_base::showpos, ios_base::uppercase, ios_base::adjustfield,

ios_base::basefield, ios_base::floatfield;

2. Элементы-функции flags(), setf(), unsetf() в классе ios_base не являются статическими, поэтому при их вызове необходимо указывать экземпляр (объект) класса или объект производного от ios_base класса, например

cout.flags(f);

3. Тип fmtflags определен в классе ios_base, поэтому вне класса следует использовать тип ios_base::fmtflags. Например, в функции вне класса можно определить переменную вида:

ios_base::fmtflags f = ios_base::showpos | ios_base::showbase | ios_base::oct |

ios_base::right;

Здесь использован оператор |объединения форматов (побитовое ИЛИ).

4. Поскольку класс ios образован из класса basic_ios являющегося производным от класса ios_base, то можно использовать записи вида

ios::scientific, ios::fixed, ios::showbase, ios::showpoint, ios::showpos, ios::uppercase, ios::adjustfield, ios::basefield, ios_base::floatfield

5. Рассмотрим функцию setf() с двумя параметрами

fmtflags setf(fmtflags f, fmtflags mask);

При ее выполнении:

- запоминается текущее значение флагов формата, это значение является возвращаемым значением функции;

- на основе текущего значения формируется новое значение флагов формата таким образом, что обнуляются те биты, которые равны 1 во втором параметре функции – маске mask;

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

Указанный подход гарантирует наличие в значении флагов только одного бита, равного 1, из множества битов маски, отличных от 0, а именно бита, равного 1, в первом параметре функции f.

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

cout.setf(ios::hex);

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

Пример 1.2.6

#include <iostream>

using namespace std;

  1.  Универсальный формат дает реализации самой выбирать формат  представления числа в том виде, который наилучшим образом представит число. Максимальное число цифр определяется точностью.
  2.  Научный формат представляет число десятичной дробью с одной цифрой перед точкой и показателем степени.
  3.  Фиксированный формат представляет число как целую часть с дробной частью, отделенной точкой. Точность определяет максимальное число цифр после точки.

По умолчанию точность равна 6 цифр для всех форматов.

Пример 1.2.7

#include <iostream>

using namespace std;

const double  pi1000 = 3141.592653589793;

int main()

  {// Формат по умолчанию (универсальный формат)

   cout << "The default format:\t" << pi1000 << '\n';

   // Научный формат

   cout.setf(ios_base::scientific, ios_base::floatfield);

   cout << "Scientific format:\t" << pi1000 << '\n';

   // Формат с фиксированной точкой

   cout.setf(ios_base::fixed, ios_base::floatfield);

   cout << "Fixed format:\t\t" << pi1000 << '\n';

   // Восстановление формата по умолчанию (универсального формата)

   cout.setf(0, ios_base::floatfield);

   cout << "The default format:\t" << pi1000 << '\n';

   return 0;}

Результаты:

    The default format:     3141.59

    Scientific format:      3.141593e+03

    Fixed format:           3141.592654

    The default format:     3141.59

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

  1. для формата по умолчанию значение представляется 6 цифрами,
  2. для научного формата выдается 6 цифр после точки,
  3. для фиксированного формата выдается 6 цифр после точки.

Для управления точностью используются следующие функции класса ios_base:

streamsize precision() const;

streamsize precision(streamsize prec);

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

Пример 1.2.8

#include <iostream>

using namespace std;

const double  pi1000 = 3141.592653589793;

int main()

  {cout <<"The default precision:\t"<< cout.precision() <<'\n';

   cout.precision(4);

   cout << "Precision:\t\t" << cout.precision() << '\n';

   // Научный формат

   cout.setf(ios_base::scientific, ios_base::floatfield);

   cout << "Scientific format:\t" << pi1000 << '\n';

   // Формат с фиксированной точкой

   cout.setf(ios_base::fixed, ios_base::floatfield);

   cout << "Fixed format:\t\t" << pi1000 << '\n';

   cout.precision(8);

   cout << "Precision:\t\t" << cout.precision() << '\n';

   // Научный формат

   cout.setf(ios_base::scientific, ios_base::floatfield);

   cout << "Scientific format:\t" << pi1000 << '\n';

   // Формат с фиксированной точкой

   cout.setf(ios_base::fixed, ios_base::floatfield);

   cout << "Fixed format:\t\t" << pi1000 << '\n';

   cout.precision(15);

   cout << "Precision:\t\t" << cout.precision() << '\n';

   // Научный формат

   cout.setf(ios_base::scientific, ios_base::floatfield);

   cout << "Scientific format:\t" << pi1000 << '\n';

   cout.precision(12);

   cout << "Precision:\t\t" << cout.precision() << '\n';

   // Формат с фиксированной точкой

   cout.setf(ios_base::fixed, ios_base::floatfield);

   cout << "Fixed format:\t\t" << pi1000 << '\n';

   // Восстановление точности по умолчанию

   cout.precision(6);

   cout <<"The default precision:\t"<< cout.precision() <<'\n';

   return 0;}

Результаты:

    The default precision:  6

    Precision:              4

    Scientific format:      3.1416e+03

    Fixed format:           3141.5927

    Precision:              8

    Scientific format:      3.14159265e+03

    Fixed format:           3141.59265359

    Precision:              15

    Scientific format:      3.141592653589793e+03

    Precision:              12

    Fixed format:           3141.592653589793

    The default precision:  6

          streamsize width() const;

          streamsize width(streamsize wide);

Тип данных streamsize определен как одна из форм целого, ширина поля задается параметром wide, а функция возвращает предыдущую ширину поля. Функция width() определяет минимальное число символов, которые выведутся следующей операцией вывода числа или строки. Полям разрешается переполняться – не происходит урезания вывода по размеру поля. Обращение к функции width() влияет только на непосредственно следующую за ней операцию вывода числа. Операция вывода сбрасывает в 0 значение ширины поля, установленное предыдущим обращением к функции width().

По умолчанию, если требуется заполнить свободные позиции поля, используются пробелы. В классе basic_ios, производном от класса ios_base (рис. 1.1), определена функция

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

Приведем пример на применение функций width(), fill() и precision():

Пример 1.2.9

#include <iostream>

using namespace std;

const double  pi1000 = 3141.592653589793;

const int intvalue = 123456789;

int main()

  {cout.width(4);

   cout << "pi1000 = " << pi1000 << ",  "

        << "intvalue = " << intvalue << '\n';

   cout.width(12);             // установка минимальной ширины поля

   cout << "Hello" << '\n';    // по умолчанию выравнивание вправо

   cout.fill('#');             // установка символа заполнения

   cout.width(12);             // установка минимальной ширины поля

   cout << "Hello" << '\n';    // по умолчанию выравнивание вправо

   cout.setf(ios_base::left, ios_base::adjustfield);// выравнивание

                                                                                                                              // влево

   cout.width(12);             // установка минимальной ширины поля

   cout << "Hello" << '\n';    // выравнивание влево

   cout.width(12);             // установка минимальной ширины поля

   cout.precision(6);          // установка точности в 6 цифр

   cout << pi1000 << '\n';

   cout.setf(ios_base::right,ios_base::adjustfield);// выравнивание

                                                     // вправо

   cout.width(12);             // установка минимальной ширины поля

   cout << intvalue << '\n';

   cout << "width = " << cout.width() << '\n';

   return 0;}

Результаты:

    pi1000 = 3141.59,  intvalue = 123456789

           Hello

    #######Hello

    Hello#######

    3141.59#####

    ###123456789

    width = 0

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

1.2.2.1.6. Приведем пример на выравнивание поля:

Пример 1.2.10

#include <iostream>

using namespace std;

1.2.2.2. Форматирование с помощью манипуляторов

Для вещественных чисел всегда выводится десятичная точка

При вводе игнорируются начальные пробельные символы

Шестнадцатеричные цифры от А до F, а также символ

Шестнадцатеричные цифры от a до f, а также символ

Вещественное число выводится в десятичном формате

Вещественное число выводится в научной

ostream_type& operator<<(ostream_type& (*pf)(ostream_type&));

ostream_type& operator<<(ios_base& (*pf)(ios_base&));

ostream_type& operator<<(ios_type& (*pf)(ios_type&));

basic_ostream<charT, traits>& flush(basic_ostream<charT, traits>& os)

{

return os.flush();  // вызов функции flush() класса basic_ostream

}

Поскольку функция flush() является членом класса basic_ostream, то эту инструкцию можно записать в виде

Пример 1.2.11

Пример 1.2.12

Пример 1.2.13

1.2.2.2.5. Для реализации манипулятора с параметром в файле iomanip.h определен шаблонный класс smanip:

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

В файле iomanip.h перегружен оператор вывода данных <<  для класса smanip. Перегруженная операторная функция описывается вне класса, поэтому имеет два параметра, первый из которых является объектом потокового типа.:

В operator<< осуществляется вызов функции по указателю, возвращается значение os. Фактический параметр вызова os имеет тип ссылки класса basic_ostream<charT, traits>. Поскольку при передаче параметра по ссылке физически передается адрес, а не сам объект, то компилятором будет выполнено преобразование типа ссылки к типу ссылки базового класса ios_base.

Рассмотрим реализацию манипуляторов с параметрами на примере выполнения инструкции

cout << setprecision(14);

Для операторной функции operator<<, определенной вне класса, это эквивалентно

operator<< (cout, setprecision(14));

В результате сначала происходит обращение к функции с параметром

smanip<int> setprecision(int n)

       { return smanip<int>(sprec, n); }

Функция sprec(), объявленная в  вызове конструктора манипулятора, имеет вид

ios_base& sprec(ios_base& str, int n)

        {str.precision(n);

          return str; }

При выполнении функции setprecision() вызывается конструктор класса smanip. В результате указатель __pf инициализируется значением указателя на функцию sprec, а переменная __manargзначением n. Возвращается объект класса smanip.

После этого выполняется тело операторной функции. Происходит обращение к функции sprec(). Операторная функция возвращает объект типа basic_ostream.

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

Общая схема перегруженных операторов вывода приведена ниже:

ostream& operator << (ostream& os, const ClassType& object)

        {

          // Инструкции подготовки элементов для вывода

          // Действительный вывод элементов

         os << // . . .

         return os;

        }

Пример 1.2.15

#include <iostream>

#include <iomanip>

using namespace std;

class Complex

 {public:

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

    Complex()

       {cout << "Constructor: Complex()" << endl;

        real = imag = 0;}

    Complex(double r, double i = 0)

       {cout << "Constructor: Complex(double r, double i = 0)"

                                                       << endl;

        real = r; imag = i;}

    double getreal() {return real;}

    double getimag() {return imag;}

  private:

    double real, imag;

 };

ostream& operator << (ostream& os, const Complex& c)

      {

       cout << "complex number: (" << c.getreal() << ","

                                   << c.getimag() << ")";

       return os;

      }

void main()


2. ОСНОВНЫЕ ТИПЫ ДАННЫХ С++ И РАБОТА С ТИПАМИ ДАННЫХ

2.1. Лексические элементы языка

В процессе компиляции текста программы на языке C++ игнорируются пробельные (whitespace) символы (если они используются не как составляющие символьных констант и строк) и комментарии, после чего остаются элементы, называемые лексемами (tokens).

К пробельным символам относятся: пробел (spaсe), горизонтальная табуляция (tab - \t), вертикальная табуляция (vertical tab - \v), перевод строки (newline - \n), возврат каретки (carriage return - \r), новая страница (formfeed - \f).

В языке C++ допускаются однострочные и многострочные комментарии. Символ // (двойная косая черта) используется в С++ для однострочного комментария. Многострочный комментарий начинается с символов /* и заканчивается символами */. Например:

for (int i = 0; i < 10; i++)          // цикл по 10 элементам

{

/*

Тело цикла

*/

}

Различаются следующие типы лексем:

  1. разделители,
  2. операторы,
  3. ключевые слова,
  4. идентификаторы,
  5. литеральные константы.

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

{}   ()   []   <>   ‘’   “”   ;   ,   :

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

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

При определении массива, организации доступа к элементам массива используются разделители [].

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

С помощью одинарных кавычек ‘’ задаются символьные константы, а с помощью двойных кавычек “” – строковые константы.

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

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

Ограничитель : применяется после спецификаторов доступа к элементам класса public, protected, private.

Операторы языка приведены в табл. 3.2.1.

Ключевые слова С++ приведены в табл. 2.1.1.

Таблица 2.1.1

and

default

friend

register

true

and_eq

delete

goto

reinterpret_cast

try

asm

do

if

return

typedef

auto

double

inline

short

typeid

bool

dynamic_cast

int

signed

typename

break

else

long

sizeof

union

case

enum

mutable

static

unsigned

catch

explicit

namespace

static_cast

using

char

export

new

struct

virtual

class

extern

operator

switch

void

const

false

private

template

volatile

const_cast

float

protected

this

wchar_t

continue

for

public

throw

while

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

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

2.2. Идентификаторы и литеральные константы

2.2.1. Идентификаторы

Идентификаторы С++ определяются следующими рекурсивными грамматическими правилами:

идентификатор::=

не_цифра

идентификатор не_цифра

идентификатор цифра

не_цифра::= a b c d e f g h i j k l m n o p q r s t u v w x y z _

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

цифра::= 0 1 2 3 4 5 6 7 8 9

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

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

2.2.2. Литеральные константы

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

литеральная_константа::=

константа_с_плавающей_точкой

целочисленная_константа

символьная_константа

строковая_константа

логическая_константа

константа_с_плавающей_точкой::=

дробная_константа <<показатель_степени>>

<<суффикс_константы_с_плавающей_точкой>>

последовательность_цифр  показатель_степени

<<суффикс_константы_с_плавающей_точкой>>

дробная_константа::=

<<последовательность_цифр>>  .  последовательность_цифр

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

показатель_степени::=

e  <<знак>>   последовательность_цифр

E  <<знак>>  последовательность_цифр

знак::= + -

последовательность_цифр::=

цифра

последовательность_цифр  цифра

суффикс_константы_с_плавающей_точкой::= один из: f l F L

целочисленная_константа::=

десятичная_константа      <<суффикс_целочисленной_константы>>

восьмеричная_константа  <<суффикс_целочисленной_константы>>

шестнадцатеричная_константа   <<суффикс_целочисленной_константы>>

десятичная_константа::=

ненулевая_цифра

десятичная_константа  цифра

восьмеричная_константа::=

0

восьмеричная_константа  восьмеричная_цифра

шестнадцатеричная_константа::=

0x  шестнадцатеричная_цифра

0X шестнадцатеричная_цифра

шестнадцатеричная_константа  шестнадцатеричная_цифра

ненулевая_цифра::= 1 2 3 4 5 6 7 8 9

восьмеричная_цифра::= 0 1 2 3 4 5 6 7

шестнадцатеричная_цифра::= 0 1 2 3 4 5 6 7 8 9 a b c d e f A B C D E F

суффикс_целочисленной_константы::=

суффикс_константы_без_знака  <<суффикс_длинной_константы>>

суффикс_длинной_константы    <<суффикс_константы_без_знака>>

суффикс_константы_без_знака::= u U

суффикс_длинной_константы::= l L

символьная_константа::=

символ

строковая_ константа::=

последовательность_символов

последовательность_символов::=

символ

последовательность_символов символ

символ::=

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

cпециальный символ

логическая_константа::= false   true

Замечание: Конструкции, содержащиеся в скобках  << >>, могут отсутствовать.

2.2.2.1. Дадим пояснения к литеральным константам. Когда величина, например, 123 встречается в программе, она называется литеральной константой. Каждая литеральная константа имеет собственный тип, например, 123 имеет тип int, а 3.14159 – тип double. Как отмечалось, литеральные константы не имеют адреса.

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

200       // десятичная запись

0310     // восьмеричная запись

0xС8    // шестнадцатеричная запись

По умолчанию литеральные целые константы трактуются как знаковые величины типа int. Они могут быть описаны как имеющие тип long, тогда за их величиной должны стоять l или L. Аналогично, литеральная целая константа будет описана как беззнаковая с типом unsigned int, если за ней следует u или U. Также можно описать литеральную константу unsigned long. Примеры:

128u        1024UL         1L         8Lu 

2.2.2.1.2. Литеральные константы с плавающей точкой могут быть представлены в экспоненциальной форме или в обычной десятичной записи. При использовании экспоненциальной формы в качестве десятичного основания используются символы e или E. По умолчанию литеральные константы с плавающей точкой трактуются как значения типа double. Для отображения констант типа float ставятся символы f или F. Для задания типа long double используются символы l или L. Примеры:

3.14159F         0.1f           12.345L          0.0

3.                     3.0             0.3 e1             

300e-2            .03e2          30e-1     

Отметим, что для констант с плавающей точкой после символа '.' допускается отсутствие цифр (3.), константы с плавающей точкой могут также начинаться с символа '.' (.03e2).

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

A’, ‘a’, ‘0’, ‘9’, ‘+’, ‘(‘, ‘)’, ‘\0’, ‘\t’, ‘\n’, ‘\’’, ‘\”’, ‘\\’

Различаются обычные и специальные символы (escape символы), использующие символ обратной косой черты \ в качестве escape символа. Специальные символы приведены в табл. 2.2.1

Таблица 2.2.1

Специальный

символ

Шестнадцатеричное

значение кода

Наименование

\n

0A

Перевод строки (newline, linefeed – NL,LF)

\t

09

Горизонтальная табуляция (tabHT)

\v

0B

Вертикальная табуляция (vertical tab - VT)

\b

08

Возврат на шаг (backspace - BS)

\r

0D

Возврат каретки (carriage return - CR)

\f

0C

Новая страница (formfeed - FF)

\a

07

Звуковой сигнал (alert –BEL)

\’

27

Апостроф (single quote)

\’’

22

Двойные кавычки (double quote)

\\

5C

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

\?

3F

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

\0

00

Нулевой символ (null character – NUL)

\ooo

Восьмеричное число (octal number)

\xhh

Шестнадцатеричное число (hex number)

Конструкция \ooo позволяет задать произвольное байтовое значение как последовательность от одной до трех восьмеричных цифр, хотя разумно всегда использовать три цифры. Конструкция \xhh позволяет задать произвольное байтовое значение как последовательность от одной до двух шестнадцатеричных цифр. Примеры

\6’  ‘\x6’  6

\60’  ‘\x30’  48

\137’  ‘\x5f’  95

2.2.2.1.4. Строковая константа при изображении заключается в двойные кавычки. Строковая константа хранится в памяти как последовательность символов, заканчивающаяся символом со значением 0 (NUL). Специальные символы внутри строки должны начинаться с символа обратной косой черты \. Например,

“”  один символ для хранения ‘\0’ – пустая строка

a”  два символа: ‘a’, ‘\0’  

ABC”  четыре символа: ‘A’, ‘B’, ‘C’, ‘\0’

\””  два символа: ‘\”’, ‘\0’

a\tb\n”  пять символов: ‘a’, ‘\t’, ‘b’, ‘\n’, ‘\0’

a\xah\129”      шесть символов: ‘a’, ‘\xa’,h’, ‘\12’, ‘9’, ‘\0’

a\xah\127”      пять символов: ‘a’, ‘\xa’,h’, ‘\127’, ‘\0’

Допускается разбиение длинных строк, например, можно записать

char letters_digits[63] = "abcdefghijklmnopqrstuvwxyz"

"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

или

char letters_digits[] = "abcdefghijklmnopqrstuvwxyz\

ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";  // в начале не должно быть пробелов

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

Пример 2.3.1

#include <iostream>

#include <typeinfo>

using namespace std;

int main(){

  cout << "typeid(123).name() = \t\t\t"

       << typeid(123).name() << endl;

  cout << "typeid(123L).name() = \t\t\t"

       << typeid(123L).name() << endl;

  cout << "typeid(123U).name() = \t\t\t"

       << typeid(123U).name() << endl;

  cout << "typeid(123UL).name() = \t\t\t"

       << typeid(123UL).name() << endl;

  cout << "typeid(3.141592653589793).name() = \t"

       << typeid(3.141592653589793).name() << endl;

  cout << "typeid(3.141592653589793F).name() = \t"

       << typeid(3.141592653589793F).name() << endl;

  cout << "typeid(3.141592653589793L).name() = \t"

       << typeid(3.141592653589793L).name() << endl;

  cout << "typeid('A').name() = \t\t\t"

       << typeid('A').name() << endl;

  cout << "typeid('\x5f').name() = \t\t\t"

       << typeid('\x5f').name() << endl;

  cout << "typeid(\"0123456789\").name() = \t\t"

       << typeid("0123456789").name() << endl;

  return 0;}

Результаты:

typeid(123).name() =                    int

typeid(123L).name() =                   long

typeid(123U).name() =                   unsigned int

typeid(123UL).name() =                  unsigned long

typeid(3.141592653589793).name() =      double

typeid(3.141592653589793F).name() =     float

typeid(3.141592653589793L).name() =     long double

typeid('A').name() =                    char

typeid('_').name() =                    char

typeid("0123456789").name() =           char[11]

2.3. Встроенные, производные и пользовательские типы данных

Каждое имя (идентификатор) в программе на С++ имеет связанный с ним тип. Тип определяет какие операции применимы к имени и как эти операции интерпретируются.

В языке введены следующие типы данных:

  1.  встроенные типы данных,
  2.  производные типы данных,
  3.  пользовательские типы данных.

2.3.1. Встроенные типы данных

2.3.1.1. Встроенные типы данных С++, для персонального компьютера с 32-х разрядной архитектурой приведены в табл. 2.3.1.

Таблица 2.3.1

Тип данных

Формат в байтах

Диапазон

Примеры

bool

1

false или true

false, true

signed char

1

-128 … 127

‘A’, ‘!’

unsigned char

1

0 … 255

200, 0310, 0xC8

short int

2

-32768 … 32767

100

unsigned short int

2

0 … 65535

0xff, 4000

int

4

-2147483648 … 2147483647

-123456789, 0xf8a432eb

unsigned int

4

0 … 4294967295

65535, 0xffff

long int

4

-2147483648 … 2147483647

0xfffff, -123456

unsigned long int

4

0 … 4294967295

0726746425, 0x75bcd15

float

4

3.4e-38 …  3.4e+38

2.35, -52.354, 1.3e+10

double

8

1.7e-308 …  1.7e+308

12.354, -78.32544, -2.5e+100

long double

10

3.4e-4932 …  1.1e+4932

8.5e-3000

Тип bool является логическим типом данных; тип char предназначен для хранения отдельных символов и небольших целых чисел; типы short int, int и long int – для целых чисел, а типы float, double, long double – для вещественных чисел (чисел с плавающей точкой). Типы char, short int, int и long int – целочисленные типы, они могут быть знаковыми (signed) и беззнаковыми (unsigned). Для знаковых типов самый левый бит зачения служит для хранения знака (0 – плюс, 1 – минус), для беззнаковых типов все биты используются для представления значения.

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

signed char  char    unsigned short int   unsigned short

short int  short    unsigned int   unsigned

long int  long     unsigned long int  unsigned long

Отрицательные целые числа представляются в дополнительном коде. Алгоритм получения дополнительного кода числа следующий. Берется двоичное представление модуля числа и инвертируются все его разряды (0 заменяются на 1 и наоборот), затем к полученному результату прибавляется 1. Например, взяв число +1 типа char (00000001), инвертировав все разряды (11111110) и прибавив 1, получаем двоичное значение –1 (11111111).

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

Пример 2.3.1

#include <iostream>

using namespace std;

template <class T>

void f(T x)

   {int Lb = sizeof(T);

    cout << "  " << hex << showbase << "\tux = ";

    if (Lb == 1)  cout << (int)x << " = " << dec << (int)x;

    else cout << x << " = " << dec << x;

    cout << endl;}

int main()

  {typedef unsigned char   un_char;

   typedef unsigned short  un_short;

   typedef unsigned int    un_int;

   typedef unsigned long   un_long;

   char   vch    = -10;

   short  vsh    = -100;

   int    vint   = -1000;

   long   vlong  = -5000;

   cout << "x(char)  = \t" << (int)vch; f<un_char>(vch);

   cout << "x(short) = \t" << vsh;      f<un_short>(vsh);

   cout << "x(int)   = \t" << vint;     f<un_int>(vint);

   cout << "x(long)  = \t" << vlong;    f<un_long>(vlong);

   return 0;}

Результаты:

    x(char)  =      -10     ux = 0xf6 = 246

    x(short) =      -100    ux = 0xff9c = 65436

    x(int)   =      -1000   ux = 0xfffffc18 = 4294966296

x(long)  =      -5000   ux = 0xffffec78 = 4294962296 

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

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

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

2.3.1.3. Пусть определены два встроенных типа type1 и type2. Тип type1 является более широким типом, чем тип type2 (тип type2 является более узким типом, чем type1), если любое значение типа type2 может быть записано в формате значения типа type1 без потери значимости значения. Для приведенных выше типов можно записать:

char < short <= int <= long < float < double < long double

Знаковые и беззнаковые типы одинакового формата считаются эквивалентными.

Приведенные соотношения определяют правила в операторах типа = для корректного присваивания значений разных типов.

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

char < short  <= int <= long

unsigned char < unsigned short <= unsigned int <= unsigned long

float < double < long double

2.3.1.4. К встроенным типам данных языка С++ также относятся массивы, рассмотренные в разделах 2.10, 2.11.

2.3.2. Производные типы данных

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

*          - модификатор определения указателя,

*const - модификатор определения константного указателя,

&        - модификатор определения ссылки,

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

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

Приведем примеры производных типов данных:

int k = 10;                      // целое

int* pk = &k;                 // указатель на int (pointer to int)

int** ppk = &pk;          // указатель на указатель на int (pointer to pointer to int)

int& kref = *pk;           // ссылка на переменную k, определенная через указатель pk

char* const p = “asdf”;  // константный указатель

int* pi = new int[10];     // указатель на массив целых чисел в свободной памяти

int (*pf1)(int* ia, int sz);                   // указатель на функцию с двумя параметрами

double* (*pf2)(double ia[], int sz);  // указатель на функцию с двумя параметрами

2.3.3. Пользовательские типы данных

К типам данных языка С++, определяемых пользователями, относятся: перечисление (enum), структура (struct), объединение (union) и класс (class). Рассмотрение этих типов данных дается в настоящей главе, а также в других главах.

2.4. Переменные 

2.4.1. Определение переменной

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

bool event;

char symbol;

int value;

double angle;

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

1. Собственно значение или rvalue (от read value – значение для чтения), которое, как и литерал, хранится в области памяти.

2. Значение адреса области памяти, ассоциированной с переменной или lvalue (от location value – значение местоположения или left value – левое значение) - место, где хранится rvalue для объекта. Адрес переменой var типа var_type определяется с помощью унарного оператора определения адреса & (address operator), поэтому адрес переменной var записывается в виде &var.

В инструкции

ch = ch – ‘0’;

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

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

double salary;

double wage;

int month;

int day;

int year;

unsigned long distance;

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

double salary, wage;

int month, day, year;

unsigned long distance;

2.4.2. Основные виды переменных

В зависимости от размещения в памяти можно выделить четыре основных вида переменных:

  1. регистровые,
  2. глобальные,
  3. локальные,
  4. размещаемые в свободной памяти.

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

for (register int i = 0; i < Limit; i++)   {/* . . . */}

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

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

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

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

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

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

new имя_типа [целое_выражение]

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

Оператор delete освобождает для дальнейшего использования в программе память, выделенную оператором new. Оператор delete задается в виде:

delete указатель_на_тип

delete [целое_выражение] указатель_на_тип

- указатель_на_тип – это указатель, возвращаемый как результат оператора new.

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

2.4.3. Инициализация переменных, три формы инициализации

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

Пример 2.4.1

#include <iostream>

using namespace std;

int int1;  double dbl1;                   // глобальные переменные

int main()

  {int int2;  double dbl2;               // локальные переменные

   int* rint =  new int;                 // переменные размещаемые в

   double* rdbl = new double;            // свободной памяти

   int* temp = new int[100];   // массив, размещаемый в свободной памяти

   for (int i = 0; i <100; i++)  temp[i] = 100 + i;

   cout << "       Global variables" << endl;

   cout<< "int1 = "<<int1<< " " <<"&int1 = " <<&int1<< " "

       << "dbl1 = "<<dbl1<< " " << "&dbl1 = "<< &dbl1 << endl;

   cout << "       Local variables" << endl;

   cout<< "int2 = "<<int2<< " " <<"&int2 = " <<&int2<< " "

       << "dbl2 = "<<dbl2<< " " << "&dbl2 = "<< &dbl2 << endl;

   cout << "       Free store variables" << endl;

   cout<< "*rint = "<<*rint << " " <<"&*rint = " <<&*rint<< " "

       << "*rdbl = "<<*rdbl<< " "<<"&*rdbl = "<<&*rdbl << endl;

   cout << "       Free store array" << endl;

   cout << "temp[0]  = "  << temp[0] <<  " "

        << "&temp[0]  = " << &temp[0]<< endl;

   cout << "temp[99] = "<< temp[99] << " "

        << "&temp[99] = " << &temp[99]<< endl;

   delete rint, rdbl;

   delete temp;

   return 0;}

Результаты:

      Global variables

int1 = 0 &int1 = 004023FC dbl1 = 0 &dbl1 = 00402400

      Local variables

int2 = 1 &int2 = 0063FE00 dbl2 = 5.43231e-312 &dbl2 = 0063FDF8

      Free store variables

*rint = 0 &*rint = 011927E0 *rdbl = 0 &*rdbl = 011927EC

      Free store array

temp[0]  = 100 &temp[0]  = 011927F8

temp[99] = 199 &temp[99] = 01192984

2.4.3.2. Начальное значение (инициализация) может быть задано при определении переменной. Для встроенных типов существуют три формы инициализации переменной типа type:

type имя_переменной = выражение;                      // первая форма

type имя_переменной (выражение);                       // вторая форма

type имя_переменной = type(выражение);             // третья форма

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

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

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

Пример 2.4.2

using namespace std;

void f()

 {typedef unsigned char   un_char;

  typedef unsigned short  un_short;

  typedef unsigned int    un_int;

  typedef unsigned long   un_long;

  typedef long double     l_double;

  bool      vb1     = true,   vb2(true),      vb3     = bool(true);

  char      vch1    = -10,    vch2(-10),      vch3    = char(-10);

  un_char   vuch1   = 10,     vuch2(10),      vuch3   = un_char(10);

  short     vsh1    = -100,   vsh2(-100),     vsh3    = short(-100);

  un_short  vush1   = 100,    vush2(100),     vush3   = un_short(100);

  int       vint1   = -200,   vint2(-200),    vint3   = int(-200);

  un_int    vuint1  = 200,    vuint2(200),    vuint3  = un_int(200);

  long      vlong1  = -300,   vlong2(-300),   vlong3  = long(-300);

  un_long   vulong1 = 300,    vulong2(300),   vulong3 = un_long(300);

  float     vfloat1 = -50.5,  vfloat2(-50.5), vfloat3 = float(-50.5);

  double    vdbl1   = -100.5, vdbl2(-100.5),  vdbl3   = double(-100.5);

  l_double vldbl1   = -200.5, vldbl2(-200.5), vldbl3  = l_double(-200.5);

 }

int main()

  {f(); return 0;}

Явную инициализацию можно применять и при определении списка переменных, например

int i = 5;

double x = 0.777 + i, y = x*sin(x), z;

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

Пример 2.4.3

using namespace std;

void f()

   {typedef unsigned char   un_char;

    typedef unsigned short  un_short;

    typedef unsigned int    un_int;

    typedef unsigned long   un_long;

    typedef long double     l_double;

    bool      vb1 = false,  vb3     = bool();

    char      vch1    = 0,  vch3    = char();

    un_char   vuch1   = 0,  vuch3   = un_char();

    short     vsh1    = 0,  vsh3    = short();

    un_short  vush1   = 0,  vush3   = un_short();

    int       vint1   = 0,  vint3   = int();

    un_int    vuint1  = 0,  vuint3  = un_int();

    long      vlong1  = 0,  vlong3  = long();

    un_long   vulong1 = 0,  vulong3 = un_long();

    float     vfloat1 = 0,  vfloat3 = float();

    double    vdbl1   = 0,  vdbl3   = double();

    l_double vldbl1   = 0,  vldbl3  = l_double();

   }

int main()

  {f(); return 0;}

Здесь переменная vb3, будет проинициализирована значением false, остальные переменные инициализируются значением 0.

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

bool vb2();

. . .

un_double vldbl2();

поскольку эти конструкции означают объявления функций.

2.5. Новые имена типов

Новые имена типов вводятся с помощью ключевого слова (спецификатора объявления) typedef:

typedef int      fmtflags;            // тип флагов форматирования

fmtflags  __fmtfl;                       // переменная типа fmtflags

typedef   long   SZ_T;    

typedef   SZ_T   streamsize;    // тип переменной задания точности и ширины поля

streamsize  __prec;                   // точность форматирования вещественных чисел

streamsize  __wide;                   // ширина поля форматирования

template<class charT>             // charT  - параметр шаблона: тип символа 

struct char_traits

       { typedef  charT     char_type;          // тип символа

         /* . . . */   };

typedef unsigned int size_t;          // size_t становится новым именем типа unsigned int

size_t size;                                     // unsigned int size;

typedef int ptrdiff_t;                     // size_t, ptrdiff_t, wchar_t, wctype_t определены в stddef.h

typedef unsigned short wchar_t;

typedef wchar_t wctype_t;

typedef int TM[10];                     // TM становится типом массива из 10 элементов int

TM m;                                           // m массив из 10 элементов типа int

typedef char* pchar;                    // pchar становится новым именем типа указатель на char

pchar p1, p2;                                 // char *p1, *p2;

typedef struct{double re, im;} complex;

complex  x;                                   // struct{double re, im;} x;

typedef  void (*FPTR)(int);        // FPTR становится новым именем типа указатель на

// функцию с параметром типа int, не вырабатывающую //значения

FPTR func_pointer;                    // void (*func_pointer)(int);  

С помощью спецификатора typedef часто удается упростить запись сложной конструкции. Рассмотрим, например, следующее объявление функции:

void (*signal(int, void(*action)(int)))(int);

Здесь задана функция signal() с двумя параметрами:

- первый параметр имеет целый тип: int,

- второй параметр является указателем action на функцию с формальным параметром типа int, с типом возвращаемого значения void.

Функция signal() возвращает значение указателя на функцию типа void(*)(int).

Упростим объявление функции:

2.6. Квалификатор типа typename

Рассмотрим описания двух классов стандартной библиотеки:

template <class T, Allocator = allocator<T>>

class vector{

  /*. . . */

public:

  typedef T                 value_type;

  typedef T*                iterator;

  /*. . . */

  iterator                  begin();

  iterator                  end();

  /*. . . */

};

template <class T, Allocator = allocator<T>>

class list{

 class link

      {/*. . . */};

 /*. . . */

public:

  typedef T                 value_type;

  typedef link*             iterator;

  /*. . . */

  iterator                  begin();

  iterator                  end();

  /*. . . */

};

Рассмотрим определения следующих функций:

void f1(vector<T> &v)

   {

    vector<T>::iterator i = v.begin();

    /*. . . */

   }

void f2(list<T> &v)

   {

    list<T>::iterator i = v.begin();

    /*. . . */

   }

Для данных функций компилятор по объявлениям vector<T>, list<T> может распознать, что

vector<T>::iterator

list<T>::iterator

являются типами данных.

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

    template <class C>

void f3(C& v)

   {

    typename C::iterator i = v.begin();

         /*. . . */

        }

С помощью квалификатора typename явно указывается, что C::iterator является типом для переменной i.

2.7. Константы

2.7.1. Константы определяемые директивой #define

Общий синтаксис директивы #define имеет вид:

#define имя_константы  значение_константы

Примеры:

#define  ASCII_A  65

#define  DAYS_IN_WEEK  7

2.7.2. Формальные константы

Синтаксис определения формальной константы типа type можно задать в виде трех форм:

const type имя_константы = выражение;                  // первая форма

const type имя_константы (выражение);                   // вторая форма

const type имя_константы = type(выражение);        // третья форма

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

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

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

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

Примеры:

сonst double pi = 3.141592653589793;     // pi является константой типа double

const int DAYS_IN_WEEK = 7;              // DAYS_IN_WEEK является константой типа int

const char str[] = {‘a’, ‘b’, ‘c’, ‘d’, ‘\0’}; // все str [i] являются константами, нельзя 

// использовать присваивания типа str[2] = ‘f’;

2.8. Перечисления

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

Рассмотрим пример, в котором безымянное перечисление

enum {sun, mon, tues, wed, thur, fri, sat, holiday = -1};

определяет имена констант со значениями от 0 (sun) до 6 (sat), значение –1 присваивается константе с именем holiday.

Именованные перечисления 

enum months {Jan = 1, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec};

enum operatorCPP {plus = '+',minus = '-',mult = '*', divide='/', rem = '%'};

позволяют вводить имена типов (months, operatorCPP), с помощью которых можно определять переменные.

Пример 2.8.1

#include <iostream>

using namespace std;

enum {sun, mon, tues, wed, thur, fri, sat, holiday = -1};

enum months {Jan = 1, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep,

                                                Oct, Nov, Dec};

enum operatorCPP {plus = '+',minus = '-',mult = '*', divide='/',

                                                    rem = '%'};

int main()

  {cout << "Безымянное перечисление: ";

   cout << sun << ", " << mon << ", " << tues << ", " << wed

        << ", " << thur << ", " << fri << ", " << sat << ", "

        << holiday << endl;

   cout << "Перечисление months: ";

   cout << Jan << ", " << Feb << ", " << Mar << ", " << Apr

        << ", " << May << ", " << Jun << ", " << Jul << ", "

        << Aug << ", " << Sep << ", " << Oct << ", "  << Nov

        << ", " << Dec << endl;

   cout << "Перечисление operatorCPP: ";

   cout << plus << ", "  << minus << ", "  << mult << ", "

        << divide << ", "  << rem << endl;

   operatorCPP oper;

   oper  = plus;     // корректно

   int i = oper;    // корректно, присваивание в переменную более широкого типа

   //oper  = 42;    // [C++ Предупреждение]: W8018 Assigning int to operatorCPP

                                           // [C++ Ошибка]: E2188 Expression syntax

   //oper  = (operatorCPP)42;         // корректно, запись значения mult

   oper  = static_cast<operatorCPP>(42);

   cout << "i = " << i << "  " << "oper = " << oper << endl;

   return 0;}

Результаты:

Безымянное перечисление: 0, 1, 2, 3, 4, 5, 6, -1

Перечисление months: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12

Перечисление operatorCPP: 43, 45, 42, 47, 37

i = 43  oper = 42

Тип enum является более узким типом, чем int. Поэтому при присваивании значения типа int переменной типа enum необходимо использовать преобразование типа. В этом примере использовано преобразование типа static_cast, о котором будет сказано ниже.

Рассмотрим значение диапазона перечисления. Пусть nмаксимально возможное значение элемента перечисления и пусть m такое минимально целое, что 2m-1 больше или равно n. Тогда верхняя граница диапазона равна 2m-1. Если наименьший элемент имеет неотрицательное значение, нижняя граница равна 0. Если наименьшее значение элемента отрицательно, нижней границей диапазона является значение -2m, где m определенное ранее значение. Для приведенных выше перечислений имеем диапазоны (-8 : 7), (0 : 15), (0 : 63). В зависимости от диапазона значений переменные типа enum могут занимать 1, 2 или 4 байта памяти, т.е. представляться в формате значений char, short, int или long.

2.9. Указатели

2.9.1. Неинициализированный и инициализированный указатели

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

2.9.1.1. Неинициализированный и инициализированный указатели (pointers) определяются следующим образом:

- неинициализированный указатель имеет вид

type* имя_ указателя;

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

typedef type* ptype;

type*  имя_указателя = &переменная;              // первая форма инициализации

type*  имя_указателя (&переменная);               // вторая форма инициализации

type* имя_указателя = ptype (&переменная);   // третья форма инициализации

Здесь

имя_указателя, переменная являются идентификаторами,

type* - тип указателя,

&      - знак оператора взятия адреса.

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

Пример 2.8.1

#include <iostream>

using namespace std;

int main()

  {typedef int* pint;

   int ival = 101;

   int* iptr1 = &ival;

   int* iptr2(&ival);

   int* iptr3 = pint(&ival);

   cout << "iptr1 = " << iptr1 << ", "

        << "*iptr1 = " << *iptr1 << endl;

   cout << "iptr2 = " << iptr2 << ", "

        << "*iptr2 = " << *iptr2 << endl;

   cout << "iptr3 = " << iptr3 << ", "

        << "*iptr3 = " << *iptr3 << endl;

   return 0;}

Результаты:

iptr1 = 0063FE00, *iptr1 = 101

iptr2 = 0063FE00, *iptr2 = 101

iptr3 = 0063FE00, *iptr3 = 101

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

2.9.1.2. Приведем примеры определения указателей разных типов:

int*        pint;                    // указатель на целое типа int

double* preal;                  // указатель на тип double

char*     pchar;                 // указатель на символ

long       lv;                        // переменная целого типа

long*     plv = &lv;           // инициализация указателя адресом переменной

Можно также ввести понятие указателя на указатель, например:

int j            = 1024;        // целое

int* pj        = &j;           // указатель на int (pointer to int)

int** ppj    = &pj;        // указатель на указатель на int (pointer to pointer to int)

Для доступа к объекту, адрес которого хранится в указателе, необходимо применить оператор *, называемый оператором косвенности, обращения по адресу или разыменования (indirection operator). Например,

int k          = *pj;         // k  содержит 1024

int m        = **ppj;      // m  содержит 1024

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

*pj = *pj + 2;              // переменная j стала содержать значение 1026

**ppj = **ppj + 4;      // переменная j стала содержать значение 1030

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

int i1 = -5,  i2 = 5,  i3 = 10;

int *pi1 = &i1,  *pi2 = &i2,  *pi3 = &i3;

Можно объединить эти инструкции в одну

int i1 = -5,  i2 = 5,  i3 = 10,  *pi1 = &i1,  *pi2 = &i2,  *pi3 = &i3;

2.9.1.4. К указателю можно прибавлять и вычитать целое значение. Прибавление к указателю 1, увеличивает содержащееся в нем значение на размер области памяти, отводимой объекту соответствующего типа. Если тип char занимает 1 байт, short – 2, int -  4 и double – 8, то прибавление 2 к указателям на char, short, int и double увеличит их значение на 2, 4, 8, 16 соответственно. Прибавление и вычитание целого значения к указателю обычно используется при работе с массивами объектов.

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

Пример 2.9.2

#include <iostream>

using namespace std;

class Coord

 {public:

    Coord(double xx, double yy) : x(xx), y(yy) {}

    double getx() const {return x;}

    double gety() const {return y;}

  private: double x, y;};

template <class Type>

ptrdiff_t dif_ptr(Type* p1, Type* p2)

   {return p2 - p1;}

Coord Gl_Obj1(-1, -1001), Gl_Obj2(1001, 1);

Coord *pGl_Obj1 = &Gl_Obj1, *pGl_Obj2 = &Gl_Obj2;

int main()

  {Coord Loc_Obj1(-11, -111), Loc_Obj2(111, 11);

   Coord *pLoc_Obj1 = &Loc_Obj1, *pLoc_Obj2 = &Loc_Obj2;

   Coord *pfst_Obj1 = new Coord(-5, -55),

         *pfst_Obj2 = new Coord(55, 5);

   cout << "pGl_Obj1 = " << pGl_Obj1 << endl;

   cout << "pGl_Obj2 = " << pGl_Obj2 << endl;

   cout << "pLoc_Obj1 = " << pLoc_Obj1 << endl;

   cout << "pLoc_Obj2 = " << pLoc_Obj2 << endl;

   cout << "pfst_Obj1 = " << pfst_Obj1 << endl;

   cout << "pfst_Obj2 = " << pfst_Obj2 << endl;

   cout << "pGl_Obj2 - pGl_Obj1 = "

        << dif_ptr(pGl_Obj1, pGl_Obj2) << endl;

   cout << "pLoc_Obj2 - pLoc_Obj1 = "

        << dif_ptr(pLoc_Obj1, pLoc_Obj2) << endl;

   cout << "pfst_Obj2 - pfst_Obj1 = "

        << dif_ptr(pfst_Obj1, pfst_Obj2) << endl;

   return 0;}

Результаты:

pGl_Obj1 = 004023E8

pGl_Obj2 = 004023F8

pLoc_Obj1 = 0063FDB8

pLoc_Obj2 = 0063FDA8

pfst_Obj1 = 0119289C

pfst_Obj2 = 011928B0

pGl_Obj2 - pGl_Obj1 = 1

pLoc_Obj2 - pLoc_Obj1 = -1

pfst_Obj2 - pfst_Obj1 = 1

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

Пример 2.9.3

#include <iostream>

using namespace std;

int f(const unsigned char* p, int N)

  {int sum = 0;

   for (int i = 0; i < N; i++) sum += p[i];

   return sum;}

int main()

  {char m[] = "abcdef";

   int Lm = (sizeof m -1)/sizeof(char);

   cout << "sum = " << f(m, Lm);

   return 0;}

Результат:

           [C++ Предупреждение]: W8079 Mixing pointers to different 'char' types

    sum = 597

В этом примере тип формального параметра (unsigned char)*, а фактического -char*, поэтому транслятор выдает предупреждение. Если тип формального параметра определить как int*, будет выдаваться ошибка.

2.9.2. Родовой указатель и операторы преобразования типов

2.9.2.1. Особое место занимает указатель типа void*. Указатель void* используется всякий раз, когда точный тип объекта или неизвестен, или изменяется в конкретных обстоятельствах. Основным применением указателя void* является передача указателей функциям, которые не содержат информации о типе объекта, а также возврат объектов “не уточненного типа” из функции. Чтобы воспользоваться таким объектом, необходимо явно преобразовать указатель типа void* в указатель на конкретный тип. Из-за  способности указателя void* адресовать объекты любого типа данных, его иногда называют обобщенным или родовым указателем.

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

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

static_cast<идентификатор_типа>(выражение)

, где

идентификатор_типа может быть типом указателя, ссылки, арифметическим типом (char, short, int, long, float, double, long double), перечисляемым типом (enum);

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

Результат выполнения оператора static_cast имеет тип идентификатор_типа. 

Замечание: cast переводится как приведение.

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

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

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

reinterpret_cast <идентификатор_типа>(выражение)

Приведем пример на использование оператора static_cast для указателя типа void*, применение оператора reinterpret_cast будет дано позднее.

Пример 2.9.4

#include <iostream>

using namespace std;

int main()

  {int iv1 = 1024;

   int* pntiv = &iv1;                      // указатель на int

   double dv1 = 3.14159;

   double* pntdv = &dv1;              // указатель на double

   char* pntch1;

   pntch1 = "TextString"; // указателю можно присваивать строковый литерал

   void* pvoid;                                    // родовой указатель

   pvoid = pntiv;          // указ. типа void* можно присваивать указ. любого типа

   int iv2= *static_cast<int*>(pvoid);        // приведение указ. к типу int*

   //int iv2= *(int*)pvoid;           // старая форма приведения указ. к типу int*

   cout << "iv1 = " << iv1 << ", iv2 = " << iv2 << endl;

   pvoid = pntdv;          // указ. типа void* можно присваивать указ. любого типа

   double dv2 =*static_cast<double*>(pvoid);              // приведение 

// указателя к типу double*

   //double dv2 = *(double*)pvoid;                  // старая форма приведения

// указателя к типу double*

   cout << "dv1 = " << dv1 << ", dv2 = " << dv2 << endl;

   pvoid = pntch1;         // указ. типа void* можно присваивать указ. любого типа

   char* pntch2 = static_cast<char*>(pvoid);         // приведение 

 // указателя к типу char*

   //char* pntch2 = (char*)pvoid;                        // старая форма приведения

// указателя к типу char*

   cout << "pntch1 = " << pntch1 << ", pntch2 = " << pntch2

                                                       << endl;

   return 0;}

Результаты: iv1 = 1024, iv2 = 1024

           dv1 = 3.14159, dv2 = 3.14159

           pntch1 = TextString, pntch2 = TextString

Приведем более содержательный пример использования родового указателя. В этом примере с помощью класса List реализуется список элементов, каждый из которых содержит указатель на данные. Чтобы не конкретизировать тип данных, указатель задается в виде void* data. При создании элемента списка этому указателю может быть присвоен указатель на любой тип данных. Однако для доступа к данным необходимо сделать приведение указателя от типа void* к истинному типу.

Пример 2.9.5

#include <iostream>

using namespace std;

class Coord

 {public:

    Coord(double xx, double yy, double zz):x(xx),y(yy),z(zz) {}

    double getx() const {return x;}

    double gety() const {return y;}

    double getz() const {return z;}

  private: double x, y, z;};

ostream& operator << (ostream& os, const Coord* c)

   {os << "Coordinate: (" <<

      c->getx() << "," << c->gety() << "," << c->getz() << ")";

    return os;}

class List

  {public:

      List(void* d, List* n): data(d), next(n) {}

      void* Data() const {return data;}

      List* getnext() const {return next;}

   private:

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

      void* data;          // указатель на данные

  };

int main()

  {// Формирование списка

   int Lel = 5;                  // число элементов списка

   List* ListHead = NULL;        // заголовок списка

   // Формирование текущих элементов списка

   // Заголовок списка ListHead указывает на последний элемент

   for (int Nel = 0; Nel < Lel; Nel++)

      ListHead = new List(

                 new Coord(Nel*3, Nel*3+1, Nel*3+2), ListHead);

   // Печать данных элементов списка

   for (List* n = ListHead; n != NULL; n = n->getnext())

      {//Преобразование указателя от void* к Coord*

       Coord* pItem = static_cast<Coord*>(n->Data());

       cout << pItem << endl;}

   return 0;}

Результаты: Coordinate: (12,13,14)

           Coordinate: (9,10,11)

           Coordinate: (6,7,8)

           Coordinate: (3,4,5)

           Coordinate: (0,1,2)

2.9.3. Создание указателя на произвольный тип

Преобразование типа позволяет работать с помощью указателя p с некоторым объектом object как с любым типом type:

type* p = (type*)&object;

Используя оператор reinterpret _cast, можно также записать

type* p = reinterpret_cast<type*>(&object);

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

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

- младшему адресу памяти (в байтах) соответствует старший байт числа (так называемая big-endian архитектура);

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

Второй способ и реализован на персональных компьютерах. Рассмотрим пример, характеризующий представление значений типа short (2 байта), long (4 байта), double (8 байт) на персональных компьютерах. Пример основан на использовании преобразований типа и возможности рассматривать каждое значение как совокупность значений отдельных байт. Результаты приведены в табл. 2.9.1.

Пример 2.9.6

#include <iostream>

#include <iomanip>

using namespace std;

typedef unsigned char un_char;

void printpointer(const un_char* p, int L)

   {cout << hex << setfill('0');

    for (int i = 0; i < L; i++)

       cout << setw(2) << (int)p[i] << " ";

    cout << dec << setfill(' ')<< endl;}

int main()

  {short i1 = 0x3210;

   long  i2 = 0x76543210;

   float f = 3.1415927;

   double d = -3.141592653589793;

   /*

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

    оператора reinterpret_cast

    un_char* p1 = reinterpret_cast<un_char*>(&i1);

    un_char* p2 = reinterpret_cast<un_char*>(&i2);

    un_char* p3 = reinterpret_cast<un_char*>(&f);

    un_char* p4 = reinterpret_cast<un_char*>(&d);

   */

   un_char* p1 = (un_char*)&i1;   un_char* p2 = (un_char*)&i2;

   un_char* p3 = (un_char*) &f;   un_char* p4 = (un_char*) &d;

   printpointer(p1,2); printpointer(p2,4);

   printpointer(p3,4); printpointer(p4,8);

   // Работа с вещественным числом типа float, заданным

   // внутренним представлением PC.

   // Логическое значение 40490fdb

   int fn = 0x40490fdb;

   float* pfn = (float*)&fn;

   cout.setf(ios_base::fixed, ios_base::floatfield);

   cout.precision(7);

   cout << "*pfn = " << *pfn << endl;

   // Работа с вещественным числом типа double, заданным

   // внутренним представлением PC.

   // Логическое значение c00921fb54442d18

   char mc[8] =  {'\x18', '\x2d', '\x44', '\x54',

                  '\xfb', '\x21', '\x09', '\xc0'};

   short ms[4] = {0x2d18, 0x5444, 0x21fb, 0xc009};

   int   mi[2] = {0x54442d18, 0xc00921fb};

   double* pdc = (double*)mc;

   double* pds = (double*)ms;

   double* pdi = (double*)mi;

   cout.precision(15);

   cout << "*pdc = " << *pdc << endl;

   cout << "*pds = " << *pds << endl;

   cout << "*pdi = " << *pdi << endl;

   return 0;}

Результаты: 10 32

           10 32 54 76

           db 0f 49 40

           18 2d 44 54 fb 21 09 c0

           *pfn = 3.1415927

           *pdc = -3.141592653589793

           *pds = -3.141592653589793

           *pdi = -3.141592653589793

Таблица 2.9.1

Тип

значения

Число

байт

Логическое значение

Представление в памяти

short

2

32 10

10 32

long

4

76 54 32 10

10 32 54 76

float

4

40 49 0f db

db 0f 49 40

double

8

c0 09 21 fb 54 44 2d 18

18 2d 44 54 fb 21 09 c0

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

2.9.5. Константные указатели

Рассмотрим несколько типов константных указателей.

           защита указуемого объекта

1) const char* p = “asdf”;    Здесь pуказатель на константный объект

   p = “ghjk”;                    - верно, указатель p можно перенацеливать на другой объект

   p[3] = ‘a’;                    - ошибка, нельзя менять содержимое памяти (константный объект)

          защита значения указателя

2) char* const p = “asdf”;     Здесь pконстантный указатель. На протяжение всей

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

содержимое памяти можно менять

   p = “ghjk”;       - ошибка, нельзя перенацеливать указатель p на другую область памяти

   p[3] = ‘a’;         - верно, в память можно писать

                защита указуемого объекта                   защита значения указателя

3) const char* const p = “asdf”;      Здесь pконстантный указатель на константный объект

   p = “ghjk”;       - ошибка, нельзя перенацеливать указатель p на другую область памяти

   p[3] = ‘a’;          - ошибка, нельзя менять содержимое памяти (константный объект)

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

1.Указатель на константный объект: const type* p = &object

const

объект1

указатель

const

объект2

2. Константный указатель: type* const p = &object

const

указатель

oбъект

3. Константный указатель на константный объект:

const type* const p = &object

const

const

указатель

oбъект

Рис.2.9.1

Допускается создание константных указателей на константы:

Пример 2.9.7

#include <iostream>

using namespace std;

int main()

  {const int i1 = 128;       // здесь под значения констант

   const int i2 = -128;      // отводится память 

   const int* pic = &i1;     // указатель на константный объект

   //int* pi = &i1;  [C++ Ошибка] E2034 Cannot convert 'const int *' to 'int *'

   cout << "&i1 = " << &i1 << " i1   = \t" << i1 << endl;

   cout << "pic = " << pic << " *pic = \t" << *pic << endl;

   //*pic = 100;   // ошибка: [C++ Error]: E2024  Cannot modify a const object.

   pic = &i2;     // константный указатель может ссылаться на другую константу

   cout << "&i2 = " << &i2 << " i2   = \t" << i2 << endl;

   cout << "pic = " << pic << " *pic = \t" << *pic << endl;

   return 0;}

Результаты:

&i1 = 0063FE00 i1   =   128

pic = 0063FE00 *pic =   128

&i2 = 0063FDFC i2   =   -128

pic = 0063FDFC *pic =   -128

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

2.9.6. Оператор преобразования константного типа

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

Выделенную с помощью оператора new память, можно связать с константным указателем. Однако при этом возникают проблемы освобождения защищенной памяти и необходимость обойти защищенность. Это называется отменой постоянства (cast away costness). Для подобных целей применяется оператор const_cast, позволяющий преобразовать константный тип в неконстантный и обратно. Оператор преобразования константного типа в неконстантный задается в виде

const_cast <идентификатор_типа>(выражение)

Здесь выражение вырабатывает константный тип. Обратное преобразование неконстантного типа в константный имеет вид

const_cast <const идентификатор_типа>(выражение)

Здесь выражение вырабатывает неконстантный тип.

Следующий пример иллюстрирует оператор приведения const_cast:

Пример 2.9.8

#include <iostream>

using namespace std;

const int TblSize = 15;

const int* iTbl;

void CreateTbl()

  {int* ip = new int[TblSize];

   /*

   for (int i = 0; i < TblSize; i++)

      {//const_cast<const int*>(ip)[i] = i + 1; добавили защиту

       // [C++ Error]: E2024 Cannot modify a const object.

       if (i != TblSize -1) cout << ip[i] << ", ";

       else cout << ip[i] << endl;

      }

   */

   iTbl = ip;    //инициализация таблицы

   cout << "Создать таблицу,   Size = " << TblSize <<

           ", указатель iTabl = " << iTbl << endl;

   for (int i = 0; i < TblSize; i++)

      {//iTbl[i] = i+1;    // [C++ Error]: E2024 Cannot modify a const object.

       const_cast<int*>(iTbl)[i] = i + 1;    // отмена защиты

       if (i != TblSize -1) cout << iTbl[i] << ", ";

       else cout << iTbl[i] << endl;}

  }

void DestroyTbl()

  {delete [] const_cast<int*>(iTbl);         // отмена защиты

   cout << "Разрушить таблицу, Size = " << TblSize <<

           ", указатель iTabl = " << iTbl << endl;}

int main()

  {CreateTbl();  DestroyTbl(); return 0;}

Результаты:

Создать таблицу,   Size = 15, указатель iTabl = 011968CC

1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15

Разрушить таблицу, Size = 15, указатель iTabl = 011968CC

В данном примере, если создать указатель

int* ip = new int[TblSize] 

, то после применения оператора

const_cast<const int*>(ip)

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

iTbl = ip;

, применить оператор

const_cast<int*>(iTbl)

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

Далее преобразование

const_cast<int*>(iTbl)

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

2.9.7. Создание указателя на указатель

Как отмечалось, указатель на указатель на переменную типа type определяется следующим образом:

type** p;

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

Пример 2.9.9

#include <iostream>

using namespace std;

int main()

  {char** p = new char*[3];

   p[0] = "First String";

   p[1] = "Second String";

   p[2] = "Third String";

   cout << p[0] << endl; cout << p[1] << endl;

   cout << p[2] << endl;

   return 0;}

Результаты:

    First String

    Second String

    Third String

Здесь p является указателем на массив из трех указателей на строки, расположенные в свободной памяти. Каждому указателю массива можно присвоить строку (рис. 2.9.2).

First String

p[0]

p

p[1]

Second String

p[2]

Third String

Рис. 2.9.2

Аналогично, можно записать:

    type** pitem;

    pitem = new type*[size];

В этом случае создается массив из size указателей на объекты типа type, указателем этого массива является pitem.

2.9.8. Указатели на объекты классов

Указатели часто используются для работы с объектами определенных пользователями типов. В приведенном ниже примере приведена шаблонная функция swap() с формальными параметрами в виде указателей на объекты класса Coord. Функция позволяет обменять содержимое двух объектов.

Пример 2.9.10

#include <iostream>

using namespace std;

class Coord

 {public:

    Coord(double xx, double yy, double zz)     // конструктор

                                    : x(xx), y(yy), z(zz) {}

    Coord(Coord& obj)                     // конструктор копирования

       {cout << "Constructor copy: Coord(Coord& obj)" << endl;

        x = obj.x; y = obj.y; z = obj.z;}

    Coord& operator = (Coord& obj)        // оператор присваивания

       {cout << "operator = (Coord& obj)" << endl;

        x = obj.x; y = obj.y; z = obj.z; return *this;}

    double getx() const {return x;}

    double gety() const {return y;}

    double getz() const {return z;}

  private: double x, y, z;};

ostream& operator << (ostream& os, const Coord* c)

  {os << "Coordinate: (" << c->getx() << ","

      << c->gety() << "," << c->getz() << ")";

   return os;}

template <typename Type>

void swap(Type* p1, Type* p2)

   {Type tmp = *p2;

    *p2 = *p1;

    *p1 = tmp;}

int main()

  {int Nel = 0;

   Coord obj1 = Coord(Nel*3, Nel*3+1, Nel*3+2);

   Nel++;

   Coord obj2 = Coord(Nel*3, Nel*3+1, Nel*3+2);

   cout << "obj1 = " << &obj1 << "  obj2 = " << &obj2<< endl;

   swap(&obj1, &obj2);

   cout << "obj1 = " << &obj1 << "  obj2 = " << &obj2<< endl;

   return 0;}

Результаты:

obj1 = Coordinate: (0,1,2)  obj2 = Coordinate: (3,4,5)

Constructor copy: Coord(Coord& obj)

operator = (Coord& obj)

operator = (Coord& obj)

obj1 = Coordinate: (3,4,5)  obj2 = Coordinate: (0,1,2)

2.10. Ссылки

2.10.1. Определение ссылки

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

type& имя_ссылки

Здесь

type& - тип ссылки, имя_ссылки – является идентификатором.

Объявление переменной в качестве ссылки на другую переменную определяет ее алиас (alias) – синоним.

Инициализация ссылки имеет три формы определения:

typedef type& rtype;                                          // определение типа ссылки

type& имя_ссылки = переменная;                  // первая форма инициализации

type& имя_ссылки (переменная);                   // вторая форма инициализации

type& имя_ссылки = rtype (переменная);       // третья форма инициализации

Здесь

имя_ссылки и переменная являются идентификаторами,

type& - тип указателя.

Приведем пример использования всех трех форм инициализации ссылки.

Пример 2.10.1

#include <iostream>

using namespace std;

int main()

  {int ival = -201;

   typedef int& rint;

   int& intref1 = ival;          // первая форма инициализации ссылки

   int& intref2(ival);           // вторая форма инициализации ссылки

   int& intref3 = rint(ival);    // третья форма инициализации ссылки

   cout << "intref1 = " << intref1 << ", "

        << "intref2 = " << intref2 << ", "

        << "intref3 = " << intref3 << endl;

   return 0;}

Результаты: intref1 = -201, intref2 = -201, intref3 = -201

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

int i         = 1;

int& iref = i;  // i и iref ссылаются на одно и то же целое

int x = iref;  // x = 1 

iref = 2;  // i = 2

Можно определить ссылку через указатель:

int k = 10;

int* pk = &k;  // указатель на переменную k

int& kref = *pk; // ссылка на переменную k, определенная через указатель pk

kref = 20;  // k = 20

2.10.1.2. Если в одной инструкции определен список ссылок, перед каждым идентификатором ссылки должен стоять символ &:

int i1 = -5,  i2 = 5,  i3 = 10;

int &ri1 = i1,  &ri2 = i2,  &ri3 = i3;

Можно объединить эти инструкции в одну

int i1 = -5,  i2 = 5,  i3 = 10, &ri1 = i1,  &ri2 = i2,  &ri3 = i3;

2.10.1.3. Принципиальное отличие ссылки от указателя заключается в том, что над указателем можно выполнять операции, а над ссылками нет. Применение любой операции к ссылке на самом деле означает применение операции к объекту, синонимом которого является ссылка. Например, хотя выражение  iref++  (эквивалентное iref = iref + 1) допустимо, оно не увеличивает ссылку iref, оператор ++ применяется только к целому значению i. Чтобы получить указатель на объект, именем которого является ссылка iref, мы можем написать

int* p = &iref;

2.10.2. Реализация ссылки

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

int i  = 1;   int& iref = i;   iref++;  int* p = &iref;

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

int i  = 1;     int* iref = &i;     (*iref)++;   int* p = &(*iref);

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

Приведенный ниже пример иллюстрирует применение приведенной выше интерпретации ссылки:

Пример 2.10.2

#include <iostream>

using namespace std;

int main()

  {int i; int &ir = i;  ir = 5;  int* pi = &ir;

   cout << "i = " << i << "  *pi = " << *pi << endl;

   int k; int* kref = &k; *kref = 5; int* pk = &(*kref);

   cout << "k = " << k << "  *pk = " << *pk << endl;

   return 0;}

Результаты:

    i = 5  *pi = 5

    k = 5  *pk = 5

Рассмотрим еще один пример, характеризующий связь между указателем и ссылкой, который был исполнен в среде C++ Builder:

Пример 2.10.3

#include <iostream>

using namespace std;

double d1   = 3.14;

double& rd1 = d1;            //смещение адреса памяти под ссылку на 8 байт

double d2   = 6.28;

double* pd1 = &d1;

int main()

 {cout << "&d1       = " << (int)&d1 << " d1 = " << d1

       << " sizeof(d1) = " << sizeof(d1) << endl;

  cout << "&d2       = " << (int)&d2 << " d2 = " << d2 << endl;

  int* p = (int*)((char*)&d1 + 8);    // указатель на память ссылки

  cout << "Value rd1 = " << *p << endl;

  cout << "Value pd1 = " << (int)pd1 << endl;

  return 0;}

Результаты:

&d1       = 4202816 d1 = 3.14 sizeof(d1) = 8

&d2       = 4202828 d2 = 6.28

Value rd1 = 4202816

Value pd1 = 4202816

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

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

Пример 2.10.4

#include <iostream>

using namespace std;

void f(int& i)

  {cout << "i   = " << i << " &i = " << &i << endl;

   i = -301;}

int main()

  {int i1 =  101,

       i2 = -101;

   int& ri1 = i1;                    // инициализация ссылки обязательна

   int& ri2 = i2;                    // инициализация ссылки обязательна

   ri1 = ri2;                        // присваивание ссылок

   cout << "ri1 = " << ri1 << ", "

        << "ri2 = " << ri2 << endl;

   int& ri3 = *new int(-201);// отведение свободной памяти и инициализация

   f(ri