Дэвис Стефан Рэнди

С++ для "чайников" .

С++ для "чайников". Дэвис Стефан Р.

Автор ФБ2( FB2 ) версии книги рерайтер

Александр Иванович Фурс

©  ( Минск. Беларусь ). 2015 г. Эл.почта:  shura_furs@mail.ru

От рерайтера:

1 ) Мною было исправлено несколько опечаток и ошибок в тексте программ и тексте оригинальной PDF книги, например

( На 168 стр. оригинала ошибка текста программы ArrayOfStudents написано Students[ 10 ] а надо Student s[ 10 ]. Пропущен пробел между t и s. ) и др.

2 ) Посталены буквы "ё" вместо буквы "е" там где они должны быть.

3 ) Построчные комментарии заменены на многострочные, что улучшает читабельность книги по сравнению с оригинальной PDF версией, особенно на мобильных устройствах.

4 ) Чёрно-белые картинки заменены на цветные скриншоты из Windows7.

5 ) Напечатал СОВЕТ ДНЯ из среды разработки Dev-C++ .

6 ) Добавил инструкцию по русификации консоли.

7 ) Сделал более подробную и глубокую навигацию по содержанию, чем у оригинала ( гиперссылки по всему тексту ).

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

9 ) В текстах программ сохранил стиль форматирования.

10 ) Сделал цветные пиктограмки.

Всё остальное как в оригинале.

Вы можете переслать любую сумму рерайтеру за переделку книги в ФБ2 (FB2)  формат, на любой из нижеприведённых WEBMONEY кошельков:

Z587187844833  ; B889951872392 ; R184950127691 ; U227750651065

или перечислив деньги на счёт:

ОАО "АСБ БЕЛАРУСБАНК" г. Минск

филиал 527 "Белжердор"

лицевой счёт №37001444

C++ for "dummies".

by Stephen Randy Davis

Publishers Since 1807

http://eu.wiley.com/WileyCDA/

WILEY

Wiley Publishing, Inc.

Стефан Р. Дэвис

С++ для "чайников". Дэвис Стефан Рэнди.

Лучшее руководство по С++ для начинающих — соответсвует последним стандартам языка!

Для сомневающихся.

На прилагаемом компакт-диске — исходные тексты всех программ и компилятор.

5-е издание.

Более 120 миллионов продано по всему миру.

ДИАЛЕКТИКА

Москва * Санкт-Петербург * Киев

2007

ББК 32.973.26-018.2.75

Д94

УДК 681.3.07

Компьютерное издательство "Диалектика"

Зав. редакцией С.Н. Тригуб

Перевод с английского и редакция канд. техн. наук И.В. Красикова

По общим вопросам обращайтесь в издательство "Диалектика" по адресу:

info@dialektika.com, http://www.dialektika.com

115419, Москва, а/я 783; 03150, Киев, а/я 152

http://www.williamspublishing.com/

Д94 , 5-е издание. : Пер. с англ. — М. : Издательский дом "Вильямс",

2007. — 384 с.: ил. — Парал. тит. англ.

ISBN 978-5-8459-0723-3 ( рус. )

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

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

ББК 32.973.26-018.2.75

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

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

Copyright © 2008 by Dialektika Computer Publishing.

Original English language edition Copyright © 2007 by Wiley Publishing, Inc.

All rights reserved including the right of reproduction in whole or in part in any form. This translation published by arrangement with Wiley Publishing, Inc.

ISBN 978-5-8459-0723-3 ( рус. )

© Компьютерное издательство "Диалектика", 2007 г.

ISBN 0-7645-6852-3 ( англ. )

© Wiley Publishing Inc.,2004

ОГЛАВЛЕНИЕ...5

СОДЕРЖАНИЕ 6

Введение 17

Часть 1. Первое знакомство с С++ 23

Глава 1. Написание вашей первой программы 25

Глава 2. Премудрости объявления переменных 41

Глава 3. Выполнение математических операций 50

Глава 4. Выполнение логических операций 55

Глава 5. Операторы управления программой 66

Часть 2. Становимся функциональными программистами 79

Глава 6. Создание функций 81

Глава 7. Хранение последовательностей в массивах 92

Глава 8. Первое знакомство с указателями в С++ 105

Глава 9. Второе знакомство с указателями 117

Глава 10. Отладка программ на С++ 128

Часть 3. Введение в классы 143

Глава 11. Знакомство с объектно-ориентированным программированием 145

Глава 12. Классы в С++ 149

Глава 13. Работа с классами 154

Глава 14. Указатели на объекты 167

Глава 15. Защищённые члены класса: не беспокоить! 181

Глава 16. Создание и удаление объектов 188

Глава 17. Аргументация конструирования 198

Глава 18. Копирующий конструктор 213

Глава 19. Статические члены 224

Часть 4. Наследование 231

Глава 20. Наследование классов 233

Глава 21. Знакомство с виртуальными функциями-членами: настоящие ли они 240

Глава 22. Разложение классов 249

Часть 5. Полезные особенности 269

Глава 23. Оператор присвоения 271

Глава 24. Использование потоков ввода-вывода 277

Глава 25. Обработка ошибок и исключения 290

Глава 26. Множественное наследование 298

Глава 27. Шаблоны С++ 308

Глава 28. Стандартная библиотека шаблонов 317

Часть 6. Великолепная десятка 329

Глава 29. Десять способов избежать ошибок 331

Глава 30. Десять основных возможностей Dev-C++ 336

Глава 31. Программа BUDGET 343

Приложение. Содержимое прилагаемого компакт-диска 379

Пиктограммы

Предметный указатель 380

СОДЕРЖАНИЕ...6

ОГЛАВЛЕНИЕ 5

Введение 17

Часть 1. Первое знакомство с С++

Глава 1. Написание вашей первой программы 25

Постигая концепции С++ 25

Что такое программа 26

Как пишут программы 26

Инсталляция Dev-C++ 27

Настройка русского языка

Как настроить русский язык в консольных программах?

Настройка Dev-C++ 30

СОВЕТ ДНЯ

Создание первой программы 31

Введение кода 32

Построение вашей программы 34

Выполнение программы 36

Dev-C++ — это не Windows 36

Помощь в Dev-C++ 36

Разбор программ 36

Определение структуры программ С++ 37

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

Использование инструкций в программах 38

Объявления 38

Генерирование вывода 39

Вычисление выражений 39

Сохранение результатов выражения 39

Обзор программы Convert продолжается... 40

Глава 2. Премудрости объявления переменных 41

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

Объявление разных типов переменных 42

Ограничения, налагаемые на целые числа в С++ 43

Округление до целых значений 43

Ограничения диапазона 43

Решение проблемы усечения дробной части 44

Ограничения, налагаемые на числа с плавающей точкой 44

Перечисление 44

Скорость вычислений 45

Потеря точности 45

Ограниченность диапазона 45

Объявления типов переменных 45

Константы 47

Специальные символы 47

Логические выражения 48

Выражения смешанного типа 48

Глава 3. Выполнение математических операций 50

Бинарная арифметика 50

Анализ выражений 51

Определение порядка операций 52

Выполнение унарных операций 53

Использование операторов присвоения 54

Глава 4. Выполнение логических операций 55

Зачем нужны логические операторы 55

Использование простых логических операторов 55

Хранение логических значений 57

Использование целых переменных в качестве логических 58

Логические операции и действительные переменные 58

Сокращённые вычисления в С++ 59

Бинарные числа в С++ 60

Десятичная система счисления 60

Другие системы счисления 60

Двоичная система счисления 60

Выражения с римскими числами 61

Выполнение побитовых логических операций 62

Побитовые операции с одним битом 62

Использование побитовых операторов 63

Простой пример 64

Практическое применение логических вычислений 65

Глава 5. Операторы управления программой 66

Управление ходом программы с помощью команд ветвления 66

Выполнение циклов 68

Цикл while 68

Использование операторов инкремента и декремента 70

Использование цикла for 71

Избегайте бесконечных циклов 73

Специальные операторы управления циклом 73

Вложенные команды управления 76

Инструкция выбора 77

Часть 2. Становимся функциональными программистами 79

Глава 6. Создание функций 81

Написание и использование функций 81

Определение функции sumSequence( ) 83

Вызов функции sumSequence( ) 83

Разделяй и властвуй 83

Подробный анализ функций 84

Простые функции 85

Функции с аргументами 85

Функции с одним аргументом 85

Функции с несколькими аргументами 87

Функция main( ) 87

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

Определение прототипов функций 89

Хранение переменных в памяти 90

Использование заголовочных файлов 91

_________________

7 стр. Содержание

Глава 7. Хранение последовательностей в массивах 92

Преимущества массивов 92

Работа с массивами 93

Инициализация массива 96

Выход за границы массива 97

Использовать ли массивы 97

Определение и использование массивов с элементами-массивами 98

Использование символьных массивов 98

Создание строки символов 99

Управление строками 100

Тип string 103

Глава 8. Первое знакомство с указателями в С++ 105

Размер переменной 105

Что такое адрес 106

Адресные операторы 106

Использование указателей 108

Сравнение указателей и почтовых адресов 109

Использование разных типов указателей 109

Передача указателей функциям 111

Передача аргументов по значению 112

Передача значений указателей 112

Передача аргументов по ссылке 113

Использование кучи 113

Область видимости 113

Проблемы области видимости 114

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

Глава 9. Второе знакомство с указателями 117

Операции с указателями 117

Повторное знакомство с массивами в свете указателей 118

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

Использование указателей для работы со строками 120

Почему при работе со строками пользуются указателями 122

Операции с указателями других типов 122

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

Объявление и использование массивов указателей 124

Использование массивов строк 124

Доступ к аргументам main( ) 126

Аргументы в DOS 127

Аргументы в Dev-C++ 127

Аргументы в Windows 127

Глава 10. Отладка программ на С++ 128

Определение типа ошибки 128

Использование отладочной печати 128

Выявление "жучка" № 1 130

Выявление "жучка" № 2 131

Использование отладчика 134

Что такое отладчик 134

_________________

8 стр. Содержание

Работа с отладчиком 134

Запуск тестовой программы 135

Пошаговое выполнение программы 136

Часть 3. Введение в классы 143

Глава 11. Знакомство с объектно-ориентированным программированием 145

Микроволновые печи и уровни абстракции 145

Приготовление блюд с помощью функций 146

Приготовление "объектно-ориентированных" блюд 146

Классификация микроволновых печей 146

Зачем нужна классификация 147

Глава 12. Классы в С++ 149

Введение в классы 149

Формат класса 149

Обращение к членам класса 150

Глава 13. Работа с классами 154

Активизация объектов 154

Моделирование реальных объектов 155

Зачем нужны функции-члены 155

Добавление функции-члена 156

Создание функции-члена 156

Именование членов класса 157

Вызов функций-членов 157

Обращение к функциям-членам 158

Доступ к членам из функции-члена 159

Именование текущего объекта 160

Разрешение области видимости 161

Определение функции-члена 162

Определение функций-членов вне класса 164

Перегрузка функций-членов 165

Глава 14. Указатели на объекты 167

Определение массивов и указателей 167

Объявление массивов объектов 168

Объявление указателей на объекты 169

Разыменование указателей на объекты 169

Использование стрелок 170

Передача объектов функциям 171

Вызов функции с передачей объекта по значению 171

Вызов функции с передачей указателя 172

Передача объекта по ссылке 173

Зачем использовать указатели и ссылки 174

Возврат к куче 175

Сравнение указателей и ссылок 175

Почему ссылки не используются вместо указателей 175

Использование связанных списков 176

Другие операции над связанным списком 177

Программа LinkedListData 178

Списки в стандартной библиотеке 180

_________________

9 стр. Содержание

Глава 15. Защищённые члены класса: не беспокоить! 181

Защищённые члены 181

Зачем нужны защищённые члены 181

Как устроены защищённые члены 182

Чем хороши защищённые члены 183

Защита внутреннего устройства класса 183

Классы с ограниченным интерфейсом 184

Обращение к защищённым членам 184

Глава 16. Создание и удаление объектов 188

Создание объектов 188

Использование конструкторов 189

Зачем нужны конструкторы 189

Работа с конструкторами 190

Конструирование одного объекта 190

Конструирование нескольких объектов 192

Конструирование составных объектов 192

Что такое деструктор 194

Зачем нужен деструктор 194

Работа с деструкторами 194

Глава 17. Аргументация конструирования 198

Как снабдить конструктор аргументами 198

Зачем конструкторам нужны аргументы 198

Как использовать конструктор с аргументами 199

Перегрузка конструктора 200

Определение конструкторов по умолчанию 203

Конструирование членов класса 204

Конструкторы константных членов 208

Управление последовательностью конструирования 208

Локальные объекты создаются последовательно 209

Статические объекты создаются один раз 209

Все глобальные объекты создаются до вызова main( ) 210

Порядок создания глобальных объектов не определён 210

Члены создаются в порядке их объявления 211

Деструкторы удаляют объекты в порядке, обратном порядку их создания 212

Глава 18. Копирующий конструктор 213

Копирование объекта 213

Зачем нужен копирующий конструктор 213

Использование конструктора копирования 214

Автоматический конструктор копирования 215

"Мелкие" и "глубокие" копии 217

Временные объекты 221

Как избегать временных объектов 222

Аргумент копирующего конструктора 223

Глава 19. Статические члены 224

Определение статических членов 224

Зачем нужны статические члены 224

Использование статических членов 225

Обращение к статическим данным-членам 226

Применение статических данных-членов 227

_________________

10 стр. Содержание

Объявление статических функций-членов 228

Что такое this 230

Часть 4. Наследование 231

Глава 20. Наследование классов 233

Зачем нужно наследование 234

Как наследуется класс 234

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

Конструирование подкласса 237

Деструкция подкласса 238

Отношение СОДЕРЖИТ 238

Глава 21. Знакомство с виртуальными функциями-членами: настоящие ли они 240

Зачем нужен полиморфизм 243

Как работает полиморфизм 245

Когда функция не является виртуальной 246

Виртуальные особенности 247

Глава 22. Разложение классов 249

Разложение 249

Реализация абстрактных классов 253

Концепция абстрактных классов 254

Создание полноценного класса из абстрактного 255

Передача абстрактных классов 257

Нужны ли чисто виртуальные функции 257

Разделение исходного кода С++ 259

Разделение программы — класс Student 260

Определение пространства имён 261

Реализация класса Student 261

Разделение программы — класс GraduateStudent 262

Реализация приложения 263

Файл проекта 264

Создание файла проекта в Dev-C++ 265

Часть 5. Полезные особенности 269

Глава 23. Оператор присвоения 271

Сравнение операторов и функций 271

Потоковые операторы 272

Мелкое копирование — глубокие проблемы 272

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

Защита от копирования 276

Глава 24. Использование потоков ввода-вывода 277

Как работают потоки ввода-вывода 277

Знакомство с подклассами fstream 278

Прямое чтение из потока 282

Что такое endl 284

Подклассы strstream 285

Работа с манипуляторами 287

_________________

11 стр. Содержание

Глава 25. Обработка ошибок и исключения 290

Зачем нужен новый механизм обработки ошибок 291

Механизм исключительных ситуаций 293

Так что же мы будем бросать? 295

Глава 26. Множественное наследование 298

Механизм множественного наследования 298

Устранение неоднозначностей множественного наследования 300

Виртуальное наследование 301

Конструирование объектов 306

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

Глава 27. Шаблоны С++ 308

Обобщение функции в шаблон 309

Шаблоны классов 311

Зачем нужны шаблоны классов 314

Советы по использованию шаблонов 316

Глава 28. Стандартная библиотека шаблонов 317

Контейнер string 317

Контейнер list 320

Итераторы 321

Использование контейнера  map 324

Часть 6. Великолепная десятка 329

Глава 29. Десять способов избежать ошибок 331

Включение всех предупреждений и сообщений об ошибках 331

Добейтесь чистой компиляции 332

Используйте последовательный стиль программирования 332

Ограничивайте видимость 332

Комментируйте свою программу 334

Хотя бы один раз выполните программу пошагово 334

Избегайте перегрузки операторов 334

Работа с кучей 334

Используйте для обработки ошибок исключительные ситуации 335

Избегайте множественного наследования 335

Глава 30. Десять основных возможностей Dev-C++ 336

Настройка редактора по вашему вкусу 336

Подсветка парных скобок 337

Включение обработки исключений 337

Включение отладочной информации 338

Создание файла проекта 338

Настройка справки 338

Переустановка точек останова после редактирования файла 339

Избегайте некорректных имён файлов 339

Включите заголовочные файлы в ваш проект 339

Работа с профайлером 339

_________________

12 стр. Содержание

Глава 31. Программа BUDGET 343

BUDGET1 343

BUDGET2 348

BUDGET3 355

Реализация модуля со связанным списком 356

Работа со счетами 358

Классы связанных списков 363

Оценка бюджета 365

BUDGET4 366

Реализация связанного списка в виде шаблона класса 366

Исходный код BUDGET4 368

Подведение итогов 373

BUDGET5 373

Использование шаблона класса из STL 373

Создание списка счетов 378

Приложение. Содержимое прилагаемого компакт-диска 379

Пиктограммы

Технические подробности

Советы.

Диск.

Помни!

Атас!

Предметный указатель 380

_________________

13 стр. Содержание

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

Об авторе

Стефан P. Дэвис ( Stephen R. Davis ) живёт с женой и сыном недалеко от Далласа, штат Техас. Он и его семья — авторы множества книг, включая такие бестселлеры, как С++ для "чайников" и С++ Weekend Crash Course. Стефан работает в компании L-3 Communications.

Благодарности

Я считаю странным то, что на обложке любой книги, особенно такой, как эта, написано только одно имя. В действительности свой труд в создание книги вкладывает громадное число людей. Для начала я хотел бы поблагодарить своего главного редактора Мэри Кордер ( Mary Corder ) и агента Клодетт Мур ( Claudette Moore ), направлявших меня при формировании материала этой книги. Во время работы над книгой я значительно повысил свой уровень как редактор и корректор, и в этом мне помогли редакторы первых изданий. И если бы не помощь координатора первого и второго изданий Сьюзанны Томас ( Suzanne Thomas ), эта книга вообще не была бы напечатана. Однако, несмотря ни на что, на обложке представлено только одно имя, а значит, ответственность за все неточности в тексте должен нести именно его обладатель.

Хочу также поблагодарить свою жену Дженни и сына Кинси за их терпение и преданность.

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

Мои две собаки, Скутер и Труди, чувствуют себя нормально, хотя Труди почти ослеп. Наши два кролика, Бивас и Батхед, отправились на большую зелёную небесную лужайку после почти полуторалетнего проживания на газоне перед нашим домом.

_________________

16 стр. Введение

ВВЕДЕНИЕ...17

ОГЛАВЛЕНИЕ

СОДЕРЖАНИЕ

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

О чём эта книга

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

В книге масса примеров. Любая рассматриваемая концепция сопровождается конкретными примерами её использования на практике — от нескольких строк кода до завершённых работоспособных программ.

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

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

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

Эта книга не обучает программированию для какой-то определённой операционной системы. Она одинаково полезна как для программиста в Windows, так и для программиста в OS/2, Unix, Linux, BeOS или иной операционной системе. Если вас интересует программирование для конкретной операционной системы, то поищите более специализированную книгу.

Прилагаемый компакт-диск

На прилагаемом к книге компакт-диске содержатся исходные тексты всех примеров из этой книги. Это избавит вас от излишней работы по их набору на клавиатуре.

Ваш компьютер не в состоянии непосредственно запустить на выполнение программу на языке С++. Сначала такая программа должна быть обработана компилятором, который создаёт из исходного текста программы исполняемый файл. ( О том, как это делается, подробно рассказывается в главе 1, "Написание вашей первой программы". )

Программы в данной книге могут быть скомпилированы любым стандартным компилятором С++, но если у вас его нет, то вам поможет прилагаемый компакт-диск, на котором имеется интегрированная среда разработчика Dev-C++. Однако это не значит, что вы должны использовать именно её.

Кроме того, на этом компакт-диске вы найдёте много интересных и полезных материалов и программ, связанных с использованием языка С++.

_________________

17 стр. Введение

Что такое С++

С++ представляет собой объектно-ориентированный низкоуровневый язык программирования, отвечающий стандартам ANSI и Международной организации стандартов ( International Standards Organization — ISO ). Объектная ориентированнось  С++ означает, что он поддерживает стиль программирования, упрощающий кодирование крупномасштабных программ и обеспечивающий их расширяемость. Будучи низкоуровневым языком, С++ может генерировать весьма эффективные высокоскоростные программы.

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

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

Соглашения, принятые в книге

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

Hi mom!

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

// программа

void main( )

{

}

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

Всяческие компьютерные сообщения, такие как команды и имена функций, будут выглядеть вот так. После имён функций всегда следуют открывающая и закрывающая скобки, например myFavoriteFunction( ). Аргументы функции в изложении обычно опускаются ( кроме случаев, когда их указание необходимо для понимания или большей ясности изложения ). Согласитесь, гораздо проще сказать "это функция myFavoriteFunction( )", чем "это функция myFavoriteFunction( int , float )". Иногда для выполнения некоторых действий в книге рекомендуется использовать специальные команды клавиатуры. Например, когда в тексте содержится инструкция: нажать <Ctrl+C>, вам следует, удерживая нажатой клавишу <Ctrl>, нажать клавишу <С>. Вводить знак "плюс" при этом не нужно.

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

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

_________________

18 стр. Введение

Как организована эта книга

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

-что представляет собой эта возможность;

-зачем она включена в язык;

-как она работает.

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

Примечание. Необходимость соблюдать формат книги требовала переноса очень длинных строк кода. В конце таких строк появляется стрелка, которая напоминает о том, что следует продолжать ввод, не торопясь нажимать клавишу <Enter>. Я очень старался свести эти длинные строки кода к минимуму.

И ещё...

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

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

Часть 1. ПЕРВОЕ ЗНАКОМСТВО С С++

Эта часть является отправной точкой нашего путешествия в мир С++. Вы начнёте его с нелёгкого испытания — написания своей первой компьютерной программы. Затем перейдёте к изучению синтаксиса языка.

Часть 2. СТАНОВИМСЯ ФУНКЦИОНАЛЬНЫМИ ПРОГРАММИСТАМИ

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

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

_________________

19 стр. Введение

Часть 3. ВВЕДЕНИЕ В КЛАССЫ

В этой части дело запутывается всё больше и больше: начинается обсуждение объектно-ориентированного программирования. По правде говоря, объектно-ориентированный подход к построению программ и есть главная причина возникновения и активного использования С++. Ведь отказавшись от объектно-ориентированных особенностей С++, мы просто возвратимся к его предшественнику — языку программирования С. В этом разделе обсуждаются такие понятия, как классы, конструкторы, деструкторы и прочие не менее "страшные" термины. Не волнуйтесь, если пока что вы не совсем понимаете, о чём идёт речь.

Часть 4. НАСЛЕДОВАНИЕ

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

Часть 5. ПОЛЕЗНЫЕ ОСОБЕННОСТИ

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

Часть 6. ВЕЛИКОЛЕПНАЯ ДЕСЯТКА

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

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

Использованные в этой книге пиктограммы :

[Технические подробности]

«Технические подробности, которые можно пропустить при первом чтении.»

_________________

20 стр. Введение

[Советы]

«Советы, которые помогут сохранить много времени и усилий.»

[Диск]

«Материал на прилагаемом компакт-диске.»

[Помни!]

«Запомните — это важно.»

[Атас!]

«Тоже важное напоминание. Это указание о том, что здесь легко допустить ошибку и даже не догадаться о ней.»

Что дальше

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

_________________

21 стр. Введение

Часть 1. ПЕРВОЕ ЗНАКОМСТВО с С++...23

ОГЛАВЛЕНИЕ

СОДЕРЖАНИЕ

Глава 1. НАПИСАНИЕ ВАШЕЙ ПЕРВОЙ ПРОГРАММЫ...25

Глава 2. ПРЕМУДРОСТИ ОБЪЯВЛЕНИЯ ПЕРЕМЕННЫХ...41

Глава 3. ВЫПОЛНЕНИЕ МАТЕМАТИЧЕСКИХ ОПЕРАЦИЙ...50

Глава 4. ВЫПОЛНЕНИЕ ЛОГИЧЕСКИХ ОПЕРАЦИЙ...55

Глава 5. ОПЕРАТОРЫ УПРАВЛЕНИЯ ПРОГРАММОЙ...66

В этой части...

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

Глава 1. НАПИСАНИЕ ВАШЕЙ ПЕРВОЙ ПРОГРАММЫ...25

ОГЛАВЛЕНИЕ

В этой главе...

►Постигая концепции С++ 25

►Что такое программа 26

►Как пишут программы 26

►Инсталляция Dev-C++ 27

►Создание первой программы 31

►Выполнение программы 36

►Разбор программ 36

►Вычисление выражений 39

Итак, мы на старте. Никого вокруг нет — только вы, я и книга. Сосредоточьтесь и постарайтесь овладеть некоторыми фундаментальными понятиями.

Компьютер — это поразительно быстрая, но невероятно глупая машина. Он может выполнить то и только то, что прикажешь ( причём с умом! ), — ни больше, ни меньше.

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

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

►Постигая концепции С++...25

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

Небольшая группа инженеров, работающих в лабораториях Белла, решила использовать фрагменты Multix в небольшой операционной системе, которую окрестили Unix ( Un-ix, Multix — словом, всё понятно? ).

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

_______________

25 стр. Глава 1. Написание вашей первой программы

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

Язык С++ включает:

►1.Семантику: словарь понятных для людей команд, которые конвертируются в машинный язык;

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

«Семантика представляет собой строительные блоки, из которых создаётся программа на С++, а синтаксис — способ собрать эти блоки в единое целое.»

[Советы]

►Что такое программа...26

Программа — это текстовый файл, содержащий последовательность команд, связанных между собой по законам грамматики С++. Этот файл называют исходным текстом ( возможно, потому, что он является началом всех наших страданий ). Исходный файл в С++ имеет расширение .СРР, так же как файлы Microsoft Word оканчиваются на .DOC или командные файлы MS DOS имеют окончание .ВАТ. Расширение .СРР всего лишь соглашение.

Задача программирования — это написание такой последовательности команд, после преобразования которой в машинный язык можно получить программу, выполняющую наши желания. Такие машинно-исполнимые программы имеют расширение .ЕХЕ[ 1 ]. Процесс превращения программы С++ в исполнимую называется компиляцией или построением ( разница между этими понятиями поясняется в главе 22, "Разложение классов" ).

Пока всё выглядит достаточно легко, не так ли? Но это лишь цветочки. Продолжим...

►Как пишут программы...26

Для написания программы вам нужны две вещи: редактор для создания исходного .СРР-файла и программа ( транслятор ), которая преобразует исходный текст в понятный машине код — .ЕХЕ-файл, выполняющий ваши команды. Инструмент ( программа-транслятор ), осуществляющий такое превращение, называется компилятором.

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

Одна из популярнейших сред разработки — Visual С++ .NET фирмы Microsoft. В ней можно скомпилировать и выполнить все программы, представленные в этой книге; однако не все из вас могут приобрести этот программный продукт из-за его довольно высокой стоимости ( кстати, многие в это не поверят, но далеко не все программисты работают в Windows — есть и другие операционные системы ).

( На сайте:

http://www.visualstudio.com/ru-ru/downloads#d-express-windows-8

Вы можете скачать совершенно бесплатно Microsoft Visual Studio Express 2013 для Windows Desktop с небольшими ограничениями, но вполне пригодной для обучения и создания программ ( правда весит эта IDE 1 Gb, но готовые Release файлы - EXE файлы получаются в десятки и сотни раз меньше чем в Dev-C++ ) и выполняются они быстрее чем откомпилированные в Dev-C++. — Прим. рерайтера. )

______________

1Как правило, но, вообще говоря, это выполняется не всегда. — Прим.ред.

_________________

26 стр. Часть 1. Первое знакомство с С++

«Существуют и общедоступные среды разработки программ С++. Одна из них — Dev-C++, которая имеется на прилагаемом компакт-диске, а самую последнюю версию вы сможете найти по адресу www.bloodshed.net.»

[Диск]

Множество свободно распространяющихся программ можно найти в Internet, некоторые из этих программ не совсем бесплатны — для их получения вы всё-таки должны будете внести небольшую сумму. За использование Dev-C++ вам не придётся ничего платить. Более подробно об условиях работы с этой средой вы можете узнать на упомянутом Web-узле.

Все программы в этой книге тестировались с использованием Dev-C++ 4.9.8.0 , но они должны без проблем работать и с другими версиями среды разработки. Вы можете зайти на мой Web-узел www.stephendavis.com и познакомиться с последней информацией по этому вопросу.

«Dev-C++ — это не какая-то урезанная и переполненная ошибками версия компилятора, разработанного непонятно кем. Это нормальная, полноценная среда программирования, поддерживающая стандарт С++ ( и способная скомпилировать все программы из данной книги, что для нас в настоящий момент самое важное ).»

[Технические подробности]

«Dev-C++ генерирует Windows-совместимые программы, но не является пакетом разработки программ для Windows в классическом понимании этого слова. Если я разгадал ваши тайные желания, то у вас нет другого выхода, кроме как приобрести коммерческий пакет наподобие Visual Studio.NET. Тем не менее, я настоятельно рекомендую сперва разобраться со всеми примерами из данной книги, до того как вы перейдёте к разработке программ для Windows.»

[Атас!]

Вам следует начать с установки Dev-C++, описанной в следующем разделе, и для практики скомпилировать свою первую программу, переводящую температуру в градусах Цельсия в температуру в градусах Фаренгейта.

«Все программы в данной книге совместимы с Visual С++ .NET ( и С++-частью Visual Studio.NET, что по сути одно и то же ). Для инсталляции Visual С++ .NET воспользуйтесь поставляемой с этим пакетом документацией. Конечно, Visual С++ .NET и Dev-C++ отличаются, но не настолько, чтобы вы не смогли работать с одной средой, хорошо зная другую.»

[Атас!]

►Инсталляция Dev-C++...27

На прилагаемом компакт-диске имеется инсталляционная программа Dev-C++. Это исполнимый файл, который находится в каталоге devcpp. Вот какие шаги следует выполнить для того, чтобы установить на своём компьютере Dev-C++.

1. Найдите на прилагаемом компакт-диске и запустите файл devcpp4980.ехе.

• Двойной щелчок на этом файле запустит инсталляцию автоматически. Заметим, что 4980 означает номер версии. Так, если вы загрузите последнюю версию Dev-С++ из Web, имя файла может отличаться от указанного здесь.

• Вы можете воспользоваться в Windows командой Start  =>  Run, ввести в диалоговом окне Run x:\devcpp\devcpp4980 , где х — буква вашего дисковода компакт-дисков.

• Установка Dev-C++ начинается с предупреждения ( рис. 1.1 ) о необходимости удалить старую версию Dev-C++, если таковая у вас установлена. ( Нехорошо, конечно, начинать знакомство с угроз, но это лучше, чем иметь неприятности потом... )

_______________

27 стр. Глава 1. Написание вашей первой программы

Рис. 1.1. Перед установкой Dev-C++ вы должны удалить старую версию

2. Если у вас не установлена предыдущая версия Dev-C++, переходите прямо к п. 4 ; в противном случае прервите инсталляцию и переходите к следующему пункту.

«Не удивляйтесь, если вы даже не слышали о Dev-C++, и тем более никогда не ставили эту среду на свой компьютер — это диалоговое окно не более чем напоминание.»

[Советы]

3. Чтобы удалить старую версию, войдите в папку Dev-C++ и дважды щёлкните на файле uninstall.ехе.

При этом запустится программа деинсталляции Dev-C++, которая удалит её с вашего компьютера, подготовив его к установке новой версии.

4. Прочтите лицензионное соглашение и щёлкните на кнопке <I Agree>, если оно не противоречит вашим жизненным принципам.

Если вы не согласитесь с лицензионным соглашением, установка Dev-C++ выполняться не будет. В противном случае вы увидите диалоговое окно, показанное на рис. 1.2, в котором вам предлагается выбрать опции установки Dev-C++. Предлагаемые по умолчанию опции вполне безобидны, надо только сделать два замечания.

• Опция Mingw compiler system... должна быть включена.

• Опция Associate С and С++ files to Dev-C++ означает, что при двойном щелчке на файле с расширением .срр будет автоматически запущена среда Dev-C++, а не какое-то иное приложение ( например, Visual С++ .NET). Отменить впоследствии такую привязку можно, но сложно.

«Не включайте эту опцию, если у вас установлен Visual Studio .NET. Программы Dev-C++ и Visual Studio .NET вполне могут сосуществовать на одном компьютере, но связывать с Dev-C++ исходные файлы программ на С++ неразумно, тем более что вы всегда можете запустить именно Dev-C++, щёлкнув на файле .срр правой кнопкой мыши и выбрав из выпадающего меню команду Open with. Впрочем, лично я как раз предпочитаю использовать описанную опцию, хотя бы просто потому, что Dev-C++ загружается гораздо быстрее Visual Studio.»

[Атас!]

5. Щёлкните на кнопке Next.

Программа инсталляции спросит вас, куда именно устанавливать Dev-C++ ( рис. 1.3 ).

6. Согласитесь с предложенным каталогом С: \Dev-Cpp.

«Не устанавливайте Dev-C++ в каталог \Program Files вместе с другими программами. Это связано с тем, что Dev-C++ плохо работает с каталогами, в имени которых есть пробелы. Так будет безопаснее.»

[Атас!]

_________________

28 стр. Часть 1. Первое знакомство с С++

Рис. 1.2. Опции установки Dev-C++, предлагаемые по умолчанию

Рис. 1.3. Размещение Dev-C++ на диске по умолчанию

7. Убедитесь, что на диске достаточно места.

Хотя Dev-C++ занимает всего лишь около 45 Мбайт дискового пространства, лучше всё же убедиться, что эти 45 Мбайт на диске имеются.

8. Щёлкните на кнопке Install.

_______________

29 стр. Глава 1. Написание вашей первой программы

Сначала покажется, что ничего не происходит. Затем Dev-C++ будет копировать свои файлы в указанный вами каталог, абсолютно ничего не помещая в каталог Windows. Конечный результат показан на рис. 1.4.

Рис. 1.4. При инсталляции Dev-C++ на диск помещается много небольших файлов

По окончании инсталляции Dev-C++ выведет диалоговое окно, в котором спросит вас, следует ли установить Dev-C++ для всех пользователей вашего компьютера? Смысл в том, что если на вашем компьютере работает кто-то ещё, то разрешаете ли вы ему пользоваться установленной вами средой Dev-C++?

9. Щёлкните на кнопке Close для завершения инсталляции.

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

Настройка русского языка

Запустите файл devcpp.exe

Нажмите Tools =>  Environment Optinons =>  Interface

В <Language> выберите русский язык и нажмите <Ок>.

Если выскочит окошко

то нажмите <Continue>.

После этого — язык интерфейса русский .

Как настроить русский язык в консольных программах?

В консольных приложениях ( похожих на текстовый режим ) русские буквы выводятся к кодировке CP-866, а в оболочке Dev-C++ они набираются в кодировке CP-1251. Это значит, что вместо русских букв вы увидите «кракозябры». Чтобы этого не случилось, кодировку нужно исправить с помощью простой дополнительной программы. Сделайте следующее:

1. Найдите на диске программы gccrus.exe и g++rus.exe ( 193 Кб ).

2. Скопируйте программы gccrus.exe и g++rus.exe в папку C:\Dev-Cpp\bin ( если при установке оболочки вы указали другой каталог вместо стандартного C:\Dev-Cpp, скопируйте программы в его подкаталог bin ).

3. Запустите оболочку Dev-C++ и войдите в меню Сервис-Параметры компилятора.

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

5. Теперь при выводе на экран можно смело использовать русские буквы, они будут «на лету» перекодироваться.

Если Вы не захотите использовать программки gccrus.exe и g++rus.exe , то в рабочих программах пишите setlocale ( LC_ALL , ".1251" ) ;, а если установите программки gccrus.exe и g++rus.exe , то в рабочих программах не пишите setlocale ( LC_ALL , ".1251" ) ;.

Если установите программки gccrus.exe и g++rus.exe и в рабочих программах напишите setlocale ( LC_ALL , ".1251" ) ;, то в консоли вместо кириллицы будут кракозябры. Выбирайте что-то одно.

Настройка Dev-C++...30

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

• Выберите команду меню Tools  =>  Compiler Options ( Сервис  =>  Параметры компилятора ).

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

10. Выберите в диалоговом окне вкладку Settings ( Настройки ).

11. Выберите в меню слева Code Generation ( Генерация кода ).

Убедитесь, что опция Enable Exception Handling ( Включить обработку исключений ) включена, как показано на рис. 1.5. ( Если она отключена, щёлкните на кнопке выпадающего списка справа и выберите Yes. )

_________________

30 стр. Часть 1. Первое знакомство с С++

Рис. 1.5. Обработка исключений должна быть включена

12. Выберите в меню слева пункт Linker ( Компоновщик ) и убедитесь, что опция Generate Debugging Information ( Генерировать отладочную информацию ) включена.

На рис. 1.6 показано, как должно выглядеть при этом диалоговое окно параметров компилятора.

Рис. 1.6. Генерация отладочной информации должна быть включена

13. Щёлкните на кнопке ОК.

На этом инсталляция завершена ( все внесённые вами изменения сохранятся автоматически ).

СОВЕТ ДНЯ

!Знаете ли вы, что...

...вы не должны отключать данные советы?

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

!Знаете ли вы, что...

...вы можете использовать vUpdate для постоянного поддержания своей среды Dev-C++ обновлённой последними исправлениями ошибок и многими приятными возможностями?

Выберите "Сервис / Проверить обновления / Пакеты..."

!Знаете ли вы, что...

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

Чтобы включить автоматическое завершение кода, войдите в "Сервис / Параметры редактора" и включите обзор классов и завершение кода.

!Знаете ли вы, что...

...Функция автоматического завершения кода срабатывает, когда вы вводите ".", " -> " или "::" в окне редактора, по истечении определённого времени?

Чтобы задать требуемый промежуток времени, войдите в "Сервис / Параметры редактора / Обзор классов / Завершение кода".

!Знаете ли вы, что...

...вы можете в любое время вызвать Функцию автоматического завершения кода нажатием "Ctrl+Пробел" в окне редактора?

( Попробуйте также нажать "Ctrl+Shift+Пробел", когда курсор находится между скобками, где перечислены аргументы Функции... )

!Знаете ли вы, что...

...Shift + щелчок мышью на элементе обзора классов даст вам объявление вместо реализации?

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

!Знаете ли вы, что...

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

Простой щелчок правой кнопкой мыши в окне обзора классов вызовет контекстное меню...

!Знаете ли вы, что...

...информация о папках пользователя из окна обзора классов хранится в Файле "classfolders.dcf" в корневом каталоге вашего проекта?

Если что-то пойдёт неправильно и возникнет путаница, просто удалите этот файл и откройте заново свой проект!

!Знаете ли вы, что...

...окно обзора имеет два режима просмотра?

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

Просто щёлкните правой кнопкой на окне обзора классов и выберите "Режим просмотра"...

!Знаете ли вы, что...

...вы можете задать, хотите ли вы открывать Файлы в редакторе одинарным или двойным щелчком в менеджере проекта?

Чтобы задать это, войдите в "Сервис / Параметры среды" и включите ( или отключите ) пункт "Открывать Файлы двойным щелчком в менеджере проекта"...

!Знаете ли вы, что...

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

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

!Знаете ли вы, что...

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

!Знаете ли вы, что...

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

Выберите "Файл / Свойства" и вы получите сведения о размере файла, числе строк, строчных комментариях, пустых строках и включаемых файлах!

!Знаете ли вы, что...

...вы можете сохранить свой проект как шаблон?

Выберите "Файл / Создать / Шаблон", это сохранит ваш проект как шаблон Dev-C++, и он будет доступен, когда вы выберете "Файл / Создать / Проект".

Этот способ даёт вам возможность начать кодирование, основываясь на личном вкусе!

!Знаете ли вы, что...

...вы можете импортировать в Dev-C++ свои проекты MS Visual С++

Достаточно выбрать "Файл / Импорт / Импорт проекта MS Visual С++". Возможно, вам придётся немного поколдовать над параметрами проекта после его импорта, но всё-таки, оно работает!

!Знаете ли вы, что...

...вы можете импортировать исходный Файл или весь проект в HTML или RTF? Этот способ даёт возможность опубликовать свои исходники на веб-сайте и сделать доступными их всему миру!

Просто щёлкните на пункте "Файл / Экспорт" и выберите метод экспорта...

!Знаете ли вы, что...

...вы можете закомментировать / раскомментировать набор строк, выделив их, а затем щёлкнуть "Правка / Закомментировать( Раскомментировать )"?

!Знаете ли вы, что...

...вы можете увеличить / уменьшить отступ набора строк, выделив его, и щёлкнуть по пункту "Правка / Увеличить ( Уменьшить отступ )"?

!Знаете ли вы, что...

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

Установите или снимите закладку щелчком на "Правка / Переключить закладку" и выборе номера закладки. Переход к закладке — щелчок на "Правка / Перейти к закладке" и выбор номера закладки.

!Знаете ли вы, что...

...в меню "Поиск" существует мощная команда "Перейти к функции"?

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

►Создание первой программы...31

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

_______________

31 стр. Глава 1. Написание вашей первой программы

Введение кода...32

При создании любой программы на С++ первым шагом становится введение команд языка с помощью текстового редактора. В среде Dev-C++ имеется встроенный редактор, разработанный специально для создания программ на С++.

1. Выберите в Windows команду Starts  =>  Programs  =>  Bloodshed Dev-C++ для запуска среды.

Интерфейс Dev-C++ выглядит так же, как и у большинства других программ для Windows.

«При запуске Dev-C++ вам пришлось немало поработать мышкой, продираясь через все эти меню. Для упрощения этого процесса можно создать ярлык для Dev-С++ на рабочем столе. Для этого дважды щёлкните на пиктограмме My Computer, затем в открывшемся окне — на диске С:, а затем — папке Dev-Cpp. После этого щёлкните на файле devcpp.ехе правой кнопкой мыши и выберите из выпадающего меню Create Shortcut. Затем перетащите созданный ярлык на рабочий стол ( или в какое-то другое легкодоступное место ). Теперь вы можете запускать Dev-С++ двойным щелчком на ярлыке.»

[Советы]

_________________

32 стр. Часть 1. Первое знакомство с С++

2. Выберите в меню Dev-C++ File  =>  New  =>  Source File ( Файл  =>  Создать  =>  Исходный файл ).

Dev-C++ откроет пустое окно, в котором вы можете ввести ваш код. Не беспокойтесь, если вы пока что не понимаете, что вводите — назначение этой книги как раз и заключается в том, чтобы всё непонятное стало простым и ясным.

3. Введите представленную ниже программу в точности так, как она приведена далее в книге.

«Пусть количество отступов и пробелов вас не волнует: не так важно, сколько пробелов вы поставили перед началом строки или между соседними словами. Однако С++ весьма чувствителен к другому: надо следить, чтобы все команды набирались в нижнем регистре[ 2 ].»

[Советы]

«Вы можете просто воспользоваться файлом Conversion.срр из каталога \Cpp_Programs\Chap01  или файлом Conversion-рус.срр из каталога \Cpp_Programs\Chap01 на прилагаемом компакт-диске.»

[Советы]

//

/* Conversion-рус.срр. Программа для преобразования градусов Цельсия в градусы Фаренгейта: Fahrenheit = Celsius*( 212 - 32 )/100 + 32 */

//

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

int main( int nNumberofArgs , char* pszArgs[ ] )

{

/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */

setlocale ( LC_ALL , ".1251" ) ;

/* Введите температуру в градусах Цельсия */

int celsius ;

cout << "Введите температуру по Цельсию: " ;

cin >> celsius ; /* строка номер 15 */

/* для приведённой формулы преобразования вычислим преобразующий множитель */

int factor ;

factor = 212 - 32 ;

/* используем вычисленный множитель для преобразования градусов Цельсия в градусы Фаренгейта */

int fahrenheit ;

fahrenheit = factor * celsius / 100 + 32 ;

/* вывод результатов */

cout << "Температура по Фаренгейту:" ;

cout << fahrenheit << endl ;

/* Пауза для того, чтобы посмотреть на результат работы программы, ожидание перед завершением программы пока пользователь не прочтёт результат и нажмёт клавишу */

system( "PAUSE" ) ;

return 0 ;

}

_____________

2В С++ отнюдь не запрещается использование символов в верхнем регистре — просто язык чувствителен к регистру, а это значит, что int main и Int Main, например, означают совершенно разные вещи. — Прим. ред.

_______________

33 стр. Глава 1. Написание вашей первой программы

4. После ввода этого кода выберите команду меню File  =>  Save As ( Файл  =>  Сохранить как... ) и сохраните файл.

Хотя это вам может показаться и не очень впечатляющим, но только что вы создали вашу первую программу на С++!

«При работе над книгой я создал каталог \Cpp_Programs, а в нём — каталог Chap01, и сохранил в нём созданный файл под именем Conversion.срр ( Conversion-рус.срр ). Обращаю ваше внимание на то, что Dev-C++ некорректно работает с дисковыми именами, в которых имеется пробел ( хорошо хоть, что Dev-C++ в состоянии работать с именами длиннее 8 символов — спасибо и за это... ).»

[Советы]

Построение вашей программы...34

После сохранения на диске исходного файла Conversion.срр самое время сгенерировать выполняемый машинный код.

Для этого нужно выбрать команду меню Execute  =>  Compile ( Выполнить  =>  Скомпилировать ) или просто нажать клавиши <Ctrl+F9> ( можно также щёлкнуть на соответствующей пиктограмме в полосе инструментов ). Dev-C++ откроет окно компиляции. Сначала ничего не происходит ( компилятор думает :) ), но через некоторое время, если программа была введена правильно, вы увидите окно, показанное на рис. 1.7. Слово Done в переводе с английского означает сделанный, в нашем случае откомпилированный ( Прим.- рер. ).

Рис. 1.7. Сообщение о компиляции программы без ошибок

Если компилятор находит ошибки в программе — а это такое же обычное дело, как снег на Чукотке — он сообщает об этом программисту. Вы обязательно столкнётесь с многочисленными предупреждениями и сообщениями об ошибках, возможно, даже при работе с простенькой программой Conversion.срр или Conversion-рус.срр . Чтобы продемонстрировать процесс исправления ошибок, изменим оператор в 15 строке cin >> celsius ; на cin >>> celsius ;.

Это нарушение кажется совсем невинным — и вы, и я вскоре бы о нём забыли. Но при компиляции открывается вкладка Compiler ( Компилятор ) с сообщением об ошибке ( рис. 1.8 ). Для того чтобы исправить ошибку, просто удалите лишний символ ">" и скомпилируйте программу заново.

_________________

34 стр. Часть 1. Первое знакомство с С++

Рис. 1.8. Сообщение об ошибке в программе

Почему С++ так требователен...35

Как видим, компилятор смог определить строку, которую мы испортили в предыдущем примере. Однако если он нашёл ошибку, то почему же он сам не решит эту проблему — и дело с концом? Ответ достаточно прост. Хотя в данном случае Dev-C++ считает, что мы всего лишь допустили опечатку при вводе символов ">>", полностью положиться на его интуицию нельзя. Ведь правильной командой в действительности может оказаться совершенно другая, не имеющая никакого отношения к ошибочной команде. Если бы компилятор исправлял ошибки так, как считает нужным, то он скрывал бы от разработчиков многие реальные проблемы.

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

«Термин parse в описании ошибки обозначает, что ошибка была найдена при проведении синтаксического анализа команд С++.»

[Советы]

_______________

35 стр. Глава 1. Написание вашей первой программы

►Выполнение программы...36

Пришло время испытания вашего нового творения. Для выполнения программы нужно запустить файл Conversion-рус.exe или Conversion.exe и обеспечить его входными данными. Полученный результат можно использовать для анализа.

Чтобы запустить программу из среды Dev-C++, нужно выбрать команду меню Ехесute  =>  Run ( Выполнить  =>  Выполнить ) или нажать <Ctrl+F10>.

При этом откроется окно, в котором вам предложат ввести температуру по Цельсию. Для проверки правильности внесите какую-то заранее известную температуру, например 100°. После нажатия клавиши <Enter> программа возвращает эквивалентную температуру по Фаренгейту, т.е. 212°:

Введите температуру по Цельсию: 100

Температура по Фаренгейту: 212

Для продолжения нажмите любую клавишу...( Press any key to continue... )

Сообщение Для продолжения нажмите любую клавишу... ( Press any key to continue... ) позволяет вам увидеть результаты работы программы перед тем, как окно будет закрыто. Нажмите <Enter>, и окно ( вместе с его содержимым ) исчезнет с экрана.

! ! ! ! ! ! ! ! ! ! !

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

! ! ! ! ! ! ! ! ! ! !

Dev-C++ — это не Windows

Заметьте, что пакет Dev-C++ не предназначен для разработки программ для Windows. Написать Windows-приложение с помощью Dev-C++ теоретически можно, но весьма непросто.

Windows-программы имеют ярко выраженный визуально-ориентированный оконный интерфейс. A Conversion.ехе является 32-битовой программой, которая выполняется в среде Windows, но Windows-программой в визуальном смысле её не назовёшь.

Если вы не знаете, чем 32-битовая программа отличается от 16-битовых, не беспокойтесь об этом. Как уже отмечалось, эта книга не о написании программ для Windows. Программы, разработанные нами в данной книге, имеют интерфейс командной строки и работают в окне MS DOS.

Начинающим Windows-программистам огорчаться не следует: ваше время не пропадёт зря. Изучение С++ совершенно необходимо как предварительное условие для написания Windows-программ.

Помощь в Dev-C++

Dev-C++ обеспечивает разработчиков системой помощи, доступной посредством команды меню Help( Справка ). В системе помощи представлена информация по различным аспектам работы в Dev-C++, но, пожалуй, наиболее существенной можно назвать справку по самому языку программирования С[ 3 ].

►Разбор программ...36

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

_______________

3К сожалению, недостаточно полная, и только на английском языке. — Прим. ред.

_________________

36 стр. Часть 1. Первое знакомство с С++

Определение структуры программ С++...37

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

/* Template.срр. ШАБЛОН. Это многострочные комментарии, которые компьютер игнорирует. */

// Или такие

// комментарии,

// однострочные.

// Template.срр

// ШАБЛОН.

// Это

// комментарии,

// которые

// компьютер

// игнорирует.

#include <cstdio>

#include <cstdlib>

#include<iostream>

using namespace std ;

int main( int nNumberofArgs , char* pzArgs[ ] )

{

/* ...здесь записывается код программы... */

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

system( "pause" ) ;

return 0 ;

}

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

«Этот код вы найдёте на прилагаемом компакт-диске в каталоге Cpp_Programs в файле Template.срр.»

[Диск]

Использование в исходном коде комментариев...37

Нетрудно заметить, что первые несколько строк Conversion.срр являются обычным текстом. Значит, или компилятор Dev-C++ оказался более понятливым, чем я его представил, или — что вероятнее всего — этот код предназначается для человеческих глаз. Оформленные таким образом строки называют комментариями. Чаще всего в комментариях программист объясняет конкретные действия, которые он собирается реализовать в следующем фрагменте кода. Компилятор комментарии игнорирует.

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

Во времена печатных машинок перевод каретки означал начало новой строки. Но ввод с клавиатуры — это не печатание на машинке. В этом случае новая строка является символом, который завершает текущую командную строку.

«Допустима и другая форма комментариев, при которой игнорируется всё, что /* заключается в такие скобки */, однако эта форма комментариев в С++ почти не используется.»

[Атас!]

_______________

37 стр. Глава 1. Написание вашей первой программы

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

Использование инструкций в программах...38

Все программы С++ в своей основе имеют то, что называют инструкциями. В этом разделе рассмотрим такие из них, которые составляют остов программы Convert.

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

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

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

«Для повышения удобочитаемости допускается добавление символов пробела в любом месте программы ( но не внутри слов! ).»

[Советы]

Игнорируя пропуски, язык С++ учитывает регистр. Например, переменные fullspeed и FullSpeed, с его точки зрения, не имеют между собой ничего общего.

Объявления...38

Строка int сelsius ; является инструкцией объявления. Объявление — это инструкция, которая определяет переменную. Переменная — это контейнер, в котором хранятся значения некоторого типа. Переменная может содержать числовые или символьные значения.

Термин "переменная" был заимствован из алгебры, где он является стереотипным для следующих выражений:

х = 10

у = 3 * х

Во втором выражении переменной у присваивается значение, определяемое формулой 3 * х. Но что такое х? Переменная х играет роль контейнера для хранения каких-то значений. В нашем случае значением х является 10 , но с таким же успехом можно определить значение х равным 20 , 30 или -1. Вторая формула имеет смысл при любом числовом значении х.

В алгебре можно начать работу непосредственно с выражения наподобие х = 10. Программируя на С++, переменную х перед её использованием необходимо объявить.

В С++ переменная имеет тип и имя. Переменная, определённая в строке 15, называется celsius. Согласно объявлению она целочисленная ( подобные названия типов, наверное, имеют целью развить у программистов ассоциативное мышление — тип int представляет собой сокращённое слово integer ).

_________________

38 стр. Часть 1. Первое знакомство с С++

Для С++ имя переменной не имеет никакого специфического значения. Имя должно начинаться с букв английского алфавита A-Z или a-z[ 4 ]. Остальные символы могут быть буквами, цифрами от 0 до 9 или подчёркивающей чертой ( _ ). Имена переменных могут быть настолько длинными, насколько это вам удобно.

«Существует негласная договорённость о том, что имена переменных должны начинаться со строчной буквы. Каждое слово внутри имени переменной пишется с прописной буквы, например myVariable.»

[Советы]

«Старайтесь давать переменным короткие, но наглядные имена. Избегайте таких имён , как х, потому что они не несут никакого смысла. Примером достаточно наглядного имени переменной может служить lengthOfLineSegment.»

[Советы]

Генерирование вывода...39

Строки, начинающиеся с cout и сin, называют инструкциями ввода-вывода, или сокращённо I/O ( input/output ) ( как и все инженеры, программисты любят сокращения и аббревиатуры ).

Первая инструкция I/O выводит фразу "Введите температуру по Цельсию" в cout ( произносится как "си-аут" — сокращённо от console output ). В С++ cout — это имя стандартного устройства вывода. В нашем случае таким устройством является монитор.

В следующей строке всё происходит с точностью до наоборот. Со стандартного устройства ввода мы получаем значение и сохраняем его в целой переменной Celsius. Стандартным устройством ввода для С++ в данном случае служит клавиатура. Этот процесс является аналогом упоминаемой выше алгебраической формулы х = 10 в С++. Программа будет считать значением celsius любое целое число, введённое пользователем.

►Вычисление выражений...39

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

Например, в программе Conversion можно назвать "вычисляющим выражением" совокупность строк с объявлением переменной factor и определением её значения как результата вычислений. Эта команда вычисляет разность между 212 и 32. В данном примере оператором является знак "минус" ( "-" ), а выражением — "212-32".

Сохранение результатов выражения...39

Разговорный язык может быть далеко не однозначным. Яркий тому пример — слово равный. Оно может употребляться в значении "одинаковый" ( например, равные силы ), а может применяться в математике для построения выражений типа "у равен утроенному х".

Чтобы избежать двусмысленности, программисты на С++ называют знак "=" оператором присвоения. Оператор присвоения сохраняет результат выражения, находящегося справа от "=", в переменной, записанной слева. Программисты говорят, что "переменной factor присвоено значение 212-32".

__________

4Имя может также начинаться с символа подчёркивания, хотя на практике это используется довольно редко. — Прим. ред.

_______________

39 стр. Глава 1. Написание вашей первой программы

«Никогда не говорите "factor  равно 212 минус 32". Такое приходится слышать от всяких ленивых типов, но мы-то с вами знаем, как говорить правильно!»

[Атас!]

Обзор программы Convert продолжается...40

Второе выражение, представленное в Conversion.срр, несколько сложнее первого. В нём используются всем известные математические символы: " * " для умножения, " / " для деления, " + " для сложения. В этом случае, однако, вычисления выполняются не просто с константами, а с переменными.

Значение переменной factor ( кстати, уже вычисленное ) умножается на значение переменной celsius ( которое было введено с клавиатуры ). Результат делится на 100 и к нему прибавляется 32. Результат всего выражения приводится к целому типу и присваивается переменной fahrenheit.

Последние команды выводят строку "Температура по Фаренгейту:" и отображают значение переменной fahrenheit.

_________________

40 стр. Часть 1. Первое знакомство с С++

Глава 2. ПРЕМУДРОСТИ ОБЪЯВЛЕНИЯ ПЕРЕМЕННЫХ...41

ОГЛАВЛЕНИЕ

В этой главе...

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

►Объявление разных типов переменных 42

►Объявления типов переменных 45

►Логические выражения 48

►Выражения смешанного типа 48

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

х = 1

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

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

К сожалению, в С++ возни с переменными несколько больше, чем в математике. Эта глава как раз и повествует о заботах, связанных с использованием переменных в С++.

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

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

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

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

int х ;

х = 10 ;

int у ;

у = 5 ;

_________________

41 стр. Глава 2. Премудрости объявления переменных

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

►Объяление разных типов переменных...42

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

х = 1 ;

х = 2.3

х = "Это - предложение."

х = Техас

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

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

int х ;

х = 1 ;

В С++ тип int определяет множество целых чисел. Напомню, что целым называется число, не имеющее дробной части.

Целые числа используют для самых разных видов вычислений. Детально этому учат в младшей школе, приблизительно до шестого класса, и лишь потом начинается путаница с дробями. Та же тенденция характерна и для С++, в котором более 90% всех переменных имеют тип int[ 5 ].

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

________

5Эта величина опять-таки существенно зависит от типа разрабатываемой программы. — Прим. ред.

_________________

42 стр. Часть 1. Первое знакомство с С++

Ограничения, налагаемые на целые числа в С++...43

Целочисленные переменные в С++ представляются типом int. На переменные этого типа накладываются те же ограничения, что и на их эквиваленты в математике.

Округление до целых значений...43

Рассмотрим проблему вычисления среднего трёх чисел. Введём три целочисленные переменные — nValue1, nValue2, nValue3. Среднее значение вычисляется по формуле

( nValue1 + nValue2 + nValue3 ) / 3

Поскольку все три значения являются целыми, их сумма тоже будет целым числом. Например, сумма чисел 1, 2 и 2 равна 5. Но если 5 поделить на 3, получим 1^2/3 , или 1,666.... В отличие от людей ( обладающих разумом ), компьютеры ( которым он свойственен далеко не всегда ) приводят полученный результат к целому значению, просто отбрасывая его дробную часть. При этом 1,666 утратит свой "дьявольский" остаток и превратится в 1.

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

( nValue1 / 3 ) + ( nValue2 / 3 ) + ( nValue3 / 3 )

Подставляя в неё те же значения 1, 2 и 2, в результате получим 0. Это случилось потому, что каждое слагаемое оказалось числом, меньшим 1. Компьютер округлил их до 0 , а сумма трёх нулей, как известно, равна 0. Так что такого приведения к целочисленным значениям, вообще говоря, нужно избегать.

Ограничения диапазона...43

Второй проблемой переменной типа int является ограниченный диапазон возможных её значений. Максимальным значением обычной целочисленной переменной является число 2147483647, минимальным — -2147483648, т.е. общий диапазон — около 4 млрд. чисел[ 6 ].

«Два миллиарда — число весьма большое, чтобы быть достаточным для большинства применений. Тем не менее есть множество задач, где этого недостаточно. Например, ваш компьютер может иметь тактовую частоту, превышающую 2 ГГц ( приставка Г — Гига — как раз и обозначает миллиард ).»

[Советы]

С++ позволяет объявлять целые числа как беззнаковые, что означает, что они не могут быть отрицательны. Целое число типа unsigned int может принимать значения от 0 до 4294967295, что иногда облегчает ситуацию.

«Вы можете объявить переменную просто как unsigned, опустив объявление int, которое подразумевается неявно.»

[Советы]

__________

6Вообще говоря, диапазон значений типа int определяется множеством факторов — в первую очередь компилятором, на выбор типа int которого оказывает огромное влияние тип компьютера, поэтому считать определённым раз и навсегда, что диапазон значений int простирается от -2^32 до +2^32-1, нельзя. — Прим. ред.

_________________

43 стр. Глава 2. Премудрости объявления переменных

Решение проблемы усечения дробной части...44

Рассмотренные особенности переменных типа int делают невозможным их использование в некоторых приложениях. Но, к счастью, С++ умеет работать и с десятичными числами, которые могут иметь ненулевую дробную часть ( математики называют их действительными числами ). Используя действительные числа, можно избежать большинства перечисленных проблем. Заметьте, что десятичные числа могут иметь ненулевую дробную часть, а могут и не иметь, оставаясь действительными. В С++ число 1.0 такое же действительное число, как и 1.5. Эквивалентным им целым числом является 1. Десятичные числа могут также быть отрицательны, например, -2.3.

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

double dValue ;

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

double dValue ;

dValue = 1.0/3.0 + 2.0/3.0 + 2.0/3.0 ;

Это эквивалентно выражению

dValue = 0.333... + 0.666... + 0.666... ;

которое даёт значение

dValue = 1.666... ;

«Я записал значение 1.666... как число с бесконечным числом шестёрок, однако на самом деле такая запись по сути невозможна в силу того, что имеется предел количества цифр, которое может быть в переменной типа double.»

[Атас!]

«На прилагаемом компакт-диске в папке Cpp_Programs\Chap02 программы IntAverage и FloatAverage демонстрируют разобранные здесь примеры вычисления среднего значения.»

[Диск]

//

/*  IntAverage — среднее 3 чисел, используя целочисленную арифметику. */

/*               Сперва сумма трёх отношений */

/*               ( сумма каждого числа разделённого на 3), */

/*               второе разделить сумму трёх чисел на 3. */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

int main( int nNumberofArgs , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */

int nValue1 ;

int nValue2 ;

int nValue3 ;

// введите три числа

cout << "Эта программа вычисляет среднее трёх чисел типа int\n "

<< "integer ( целочисленной ) арифметики\n\n" ;

cout << "Введите три целых числа:\n" ;

cout << "n1 - " ;

cin  >> nValue1 ;

cout << "n2 - " ;

cin  >> nValue2 ;

cout << "n3 - " ;

cin  >> nValue3 ;

/*  Сперва сумма трёх отношений */

cout << "n1/3 + n2/3 + n3/3 = " ;

cout << nValue1/3 + nValue2/3 + nValue3/3 ;

cout << "\n" ;

/* Сейчас соотношение трёх сумм */

cout << "( n1 + n2 + n3 ) / 3   = " ;

cout << ( nValue1 + nValue2 + nValue3) / 3 ;

cout << "\n" ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ;

return 0 ;

}

/* FloatAverage — среднее 3 чисел, используя арифметику с плавающей точкой.*/

/*                   В противном случае, так же как IntAverage */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

int main( int nNumberofArgs , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */

float fValue1 ;

float fValue2 ;

float fValue3 ;

// введите три числа

cout << "Эта програма вычисляет среднее трёх чисел типа float\n"

<< "floating point( с плавающей точкой ) арифметики\n\n" ;

cout << "Введите три числа:\n" ;

cout << "f1 - " ;

cin  >> fValue1 ;

cout << "f2 - ";

cin  >> fValue2 ;

cout << "f3 - " ;

cin  >> fValue3 ;

/* Сперва сумма трёх отношений */

cout << "n1/3 + n2/3 + n3/3 = " ;

cout << fValue1/3 + fValue2/3 + fValue3/3 ;

cout << "\n" ;

/* Сейчас соотношение трёх сумм */

cout << "(n1 + n2 + n3)/3   = " ;

cout << (fValue1 + fValue2 + fValue3) / 3 ;

cout << "\n" ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ;

return 0 ;

}

Ограничения, налагаемые на числа с плавающей точкой...44

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

Перечисление...44

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

_________________

44 стр. Часть 1. Первое знакомство с С++

Например, ясно, что 1.0 есть 1. Но что такое 0.9 или 1.1 ? Следует ли их рассматривать как 1 ? Так что С++ избегает многих проблем, требуя использовать при перечислении только целые значения.

Скорость вычислений...45

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

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

Потеря точности...45

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

Чтобы понять эту проблему, представим 1/3 в виде бесконечной последовательности 0.333.... Однако математическое понятие периода в программировании не имеет смысла, так как точность компьютерных вычислений ограничена и где-то наша дробь должна оборваться ( что зависит от использованного для хранения числа типа переменной ). Поэтому, усреднив числа 1, 2, 2, мы получим не точное, а приблизительное значение 1.666667.

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

Ограниченность диапазона...45

Тип данных double также ограничен, хотя его диапазон намного обширнее диапазона целочисленных переменных. Максимальным значением типа int является число чуть больше 2 млрд.; максимальное значение переменной типа double приблизительно равно 10^308, т.е. 1 с 308 нулями[ 7 ].

«Представляя переменные с плавающей точкой в стандартном виде, С++ учитывает после десятичной точки только первые 13 разрядов. Остальные 25 разрядов становятся жертвами ошибочных округлений.»

[Атас!]

►Объявления типов переменных...45

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

________

7Это не означает, будто тип double может представить 10^38 разных значений; вспомните, что говорилось выше о количестве разрядов в числах этого типа. — Прим. ред.

_________________

45 стр. Глава 2. Премудрости объявления переменных

Таблица 2.1. Переменные С++

_________________

Переменная — Пример — Характеристика

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

int — 1 — Простые положительные или отрицательные числа,  используемые для перечисления .

unsigned — 1U — Неотрицательные числа, предназначенные в первую очередь для перечислений.

long — 10L — Потенциально расширенная версия типа int. В Dev-C++ и Microsoft Visual С++ .NET разницы между типами long и int нет.

unsigned long —10UL — Неотрицательная версия типа long.

float  —  1.0F —  Действительные числа единичной точности. Это уменьшенная версия double , требующая меньшего количества памяти, но при этом имеющая меньшую точность и диапазон возможных значений.

double  —  1.0 — Стандартное представление чисел с плавающей точкой.

char — c  — Символьный тип; значением переменных может быть символ алфавита, цифра, знак препинания или знак арифметической операции. Не годится для арифметических операций.

string   —  "this is a  string" —  Строка символов, составляющая предложение.

bool  — true  —  Тип имеет только два логических значения — true и false ( истина и ложь ).

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

Может показаться странным, что основным типом для действительных чисел является тип двойной точности, а не одинарной. Тому есть исторические причины — уходящие корнями в те времена, когда Большой Билл утверждал, что 640 Кбайт памяти хватит всем и всегда, а экономия памяти и повышение скорости вычислений за счёт точности представлялась разумной мерой. С удешевлением памяти и наращиванием мощности процессоров ( которые теперь работают с типом float с той же скоростью, что и с double ) тип double стал стандартом для чисел с плавающей точкой.

Следующий оператор объявляет переменные lVariable типа long и dVariable типа double и присваивает им начальные значения:

/* объявление переменной типа long и установка её равной 1 */

long lVariable ;

lVariable = 1

/* объявление переменной типа double и её инициализация */

double dVariable ;

dVariable = 1.0 ;

«Объявить и инициализировать переменную можно одним оператором:

int nVariable = 1 ;

/* объявление переменной и её инициализация */

»

[Советы]

Единственное преимущество инициализации переменной в объявлении — уменьшение размеров текстов программ.

Переменная типа char может содержать единственный символ, в то время как строковая переменная — строку символов. Поэтому а можно интерпретировать и как символ а, и как строку, содержащую только букву а . В главе 9, "Второе знакомство с указателями", вы найдёте детальное описание этого типа данных. В качестве грубой аналогии можно рассматривать символ 'а' как отдельный патрон, а строку "а" — как автомат, в магазине которого имеется только один патрон.

_________________

46 стр. Часть 1. Первое знакомство с С++

«Символ а и строка а — это далеко не одно и то же. Если вы захотите присвоить символьной переменной строковое значение ( или наоборот ), вы не сможете этого сделать даже в том случае, когда строка содержит единственный символ.»

[Атас!]

Константы...47

Константой называют произвольную постоянную величину ( например, 1, 0.5 или 'с' ). Подобно переменным, константы имеют свой тип. В выражении n = 1 ; константа 1 имеет тип int. Чтобы привести 1 к типу long, нужно написать n = 1L ; . Для лучшего понимания можно провести следующую аналогию: если под 1 подразумевать поездку на грузовике, то 1L можно интерпретировать как путешествие на лимузине. Их маршруты могут быть совершенно одинаковыми, но согласитесь, что путешествовать вторым способом гораздо удобнее.

Константу 1 можно также привести к действительному числу 1.0. Однако заметим, что по умолчанию типом действительной константы является double. Поэтому 1.0 будет числом типа double , а не float.

«Величина true представляет собой константу типа bool, но "true" ( обратите внимание на кавычки ) — это уже строка. Кроме того, с учётом чувствительности С++ к регистру, true — это константа типа bool, a TRUE может быть чем угодно ( тем, чем объявит программист ).»

[Атас!]

Специальные символы...47

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

С символом новой строки вы уже встречались раньше. Он позволяет разделить строку в любом месте на две части. Например, строка "Это первая строка\nЭто вторая строка" при выводе на экран будет выглядеть так:

Это первая строка

Это вторая строка

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

_________________

47 стр. Глава 2. Премудрости объявления переменных

Коллизии между С++ и именами файлов MS DOS...48

В MS DOS для разделения имён файлов в указаниях пути используется символ обратной косой черты. Так, root\folderА\file представляет собой путь к файлу file в папке folderA, которая является подкаталогом каталога root.

К сожалению, функциональное предназначение обратной косой черты в MS DOS и С++ не совпадает. Обратная косая черта в С++ используется для обозначения управляющих символов, а её саму можно вывести с помощью символов \\. Поэтому путь MS DOS root\folderA\file должен быть представлен в С++ строкой "root\\folderA\\file".

►Логические выражения...48

С++ предоставляет в распоряжение программиста логический тип bool. Название этого типа происходит от имени Буля, автора символьной логики. Булева переменная может иметь только одно из двух значений — true или false.

«В С++ имеются выражения, которые дают результат типа bool — например, выражение "х равно у" может иметь значение true или false.»

[Советы]

►Выражения смешанного типа...48

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

/* в следующем выражении перед выполнением операции сложения значение nValue1 преобразуется к типу double */

int nValue1 = 1 ;

double dValue = nValue1 + 1.0 ;

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

/*в следующем задании целая часть dVariable сохраняется в nVariable */

double dVariable = 1.0 ;

int nVariable ;

nVariable = dVariable ;

«Если переменная в левой стороне равенства относится к типу менее мощному, чем переменная справа, то при таком присвоении можно потерять точность или диапазон значений ( например, если значение переменной dVariable превышает диапазон допустимых значений переменной nVariable ).»

[Атас!]

Преобразование типа большего размера в меньший называется понижающим приведением ( demotion ), а обратное преобразование — повышающим приведением ( promotion ).

_________________

48 стр. Часть 1. Первое знакомство с С++

Соглашения по именованию

Вы могли заметить, что имя каждой переменной начинается с определённого символа, который, как может показаться, совсем ни к чему ( эти специальные символы приведены в таблице ниже ). С помощью соглашений по использованию этих символов можно мгновенно распознать, что dvariable — это переменная типа double. Данные символы помогают программисту распознавать типы переменных, не обращаясь к их объявлениям в другом месте программы. Так, нетрудно определить, что в представленном ниже выражении осуществляется присвоение смешанного типа ( переменная типа long присваивается целочисленной переменной ):

nVariable = lVariable ;

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

double myVariable ;

int someIntValue ;

double nThisDoesntEvenMatch ;

Символ —Тип

n — int

l — long

f — float

d — double

с —character

sz — string

«Использование в С++ выражений смешанного типа — идея далеко не самая блестящая; их лучше избегать, не позволяя С++ делать преобразования за вас.»

[Советы]

_________________

49 стр. Глава 2. Премудрости объявления переменных

Глава 3. ВЫПОЛНЕНИЕ МАТЕМАТИЧЕСКИХ ОПЕРАЦИЙ...50

ОГЛАВЛЕНИЕ

В этой главе...

►Бинарная арифметика 50

►Анализ выражений 51

►Определение порядка операций 52

►Выполнение унарных операций 53

►Использование операторов присвоения 54

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

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

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

int var1 ;

int var2 = 1 ;

var1 = 2 * var2 ;

В этом примере объявлены две переменные, var1 и var2. Переменной var2 присвоено начальное значение 1, var1 определена как результат удвоения переменной var2.

В этой главе вы найдёте описание всего множества математических операторов С++.

►Бинарная арифметика...50

Бинарными называются операторы, которые имеют два аргумента. В выражениях типа var1 op var2 оператор op бинарный. Самыми распространёнными бинарными операторами являются простые математические операции, изучаемые ещё за школьными партами. Бинарные операции, которые поддерживает С++, приведены в табл. 3.1.

Таблица 3.1. Математические операции в порядке приоритета

_________________

Приоритет — Оператор — Значение

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

1 — + ( унарный ) — Реально ничего не изменяет

1 — - ( унарный ) — Возвращает противоположное по знаку, равное по модулю значение

2 — ++ ( унарный ) — Оператор инкремента, увеличивает значение аргумента на 1

_________________

50 стр. Часть 1. Первое знакомство с С++

2 — -- ( унарный ) — Оператор декремента, уменьшает значение аргумента на 1

3 — * ( бинарный ) — Умножение

3 — / ( бинарный ) — Деление

3 — % ( бинарный ) — Остаток ( деление по модулю )

4 — + ( бинарный ) — Сложение

4 — - ( бинарный ) — Вычитание

5 — =, *=, %=, +=, -= ( специальные ) — Операторы присвоения

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

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

float var = 133 / 12 ;

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

По своей сути этот оператор означает получение остатка от деления. Например, 4 входит в 15 три раза, и остаток при этом составляет 3. Выражаясь терминами С++, 15, делённое по модулю 4, равно 3.

int var = 15 % 4 ; /*переменной var присваивается значение 3 */

Программисты всегда пытаются удивить непрограммистов, а потому в С++ деление по модулю определяется так:

IntValue % IntDivisor

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

IntValue - ( IntValue / IntDivisor ) * IntDivisor

Вот пример:

15 % 4 равно     15 - ( 15/4 ) * 4

15 - 3 * 4

15 - 12

3

«Для действительных переменных оператор деления по модулю не определён, поскольку он целиком основан на использовании округления ( округления рассматривались в главе 2, "Премудрости объявления переменных" ).»

[Атас!]

►Анализ выражений...51

Самый распространённый вид инструкций в С++ — выражение. Выражением в С++ называют любую последовательность операторов ( длиной не меньше одного ), которая возвращает значение. Все выражения типизированы. Тип выражения определяется типом возвращаемого значения. Например, значение выражения 1 + 2 равняется 3, следовательно, это целочисленное выражение ( тут нужно вспомнить, что константы без дробной части определяются как имеющие тип int ). Синтаксическая конструкция, включающая математический оператор, является выражением, так как в результате выполнения любой операции получается число.

_________________

51 стр. Глава 3. Выполнение математических операций

Выражения бывают как сложными, так и крайне простыми. С++ понимает под выражением любой завершённый оператор. Поэтому корректным оператором является, например, 1 ;. Он тоже представляет собой выражение, потому что его значение 1, а тип int. В операторе

z = х * у + w;

можно выделить пять выражений:

x * у + w

x * у

x

y

w

Необычный аспект С++ состоит в том, что выражение само по себе является завершённой инструкцией, т.е. упомянутое выражение 1 ; — завершённая инструкция С++.

►Определение порядка операций...52

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

int var = 2 * 3 + 1 ;

Если сложение выполнить перед умножением, то значением выражения будет 2 * 4 = 8. Если сперва выполнить умножение, то получим значение 6 + 1 = 7.

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

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

int var = 8 / 4 / 2 ;

Как в этом случае следует поступить: сначала 8 поделить на 4 или 4 на 2? Если в одном выражении присутствуют операции с одинаковыми приоритетами, они выполняются слева направо ( то же правило применяется и в арифметике ). Поэтому в предыдущем примере сперва делим 8 на 4, получая 2, а затем делим его на 2, получая ответ — 1.

В выражении

х / 100 + 32

х делится на 100 и к результату прибавляется 32. Но что, если программисту нужно поделить х на сумму 100 и 32? В таком случае ему придётся использовать скобки:

х / ( 100 + 32 )

При вычислении такого выражения х будет делиться на 132. Заметим, что начальное выражение х / 100 + 32 идентично следующему:

( х / 100 ) + 32

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

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

_________________

52 стр. Часть 1. Первое знакомство с С++

►Выполнение унарных операций...53

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

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

int var1 = 10 ;

int var2 = -var1 ;

Здесь в последнем выражении используется унарный оператор "-".

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

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

/*  Инициализация переменной  */

int var = 10 ;

/*  Её увеличение; значение  переменной равно 11 */

var++ ;

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

Предположим, что переменная n имеет значение 5. Оба способа применения к n оператора инкремента ( ++n и n++ ) приведут к результату 6. Разница между ними состоит в том, что значение n в выражении ++n равно 6, в то время как в выражении с постфиксной формой записи оно равно 5. Это можно проиллюстрировать следующим примером:

/* объявляем три целые переменные */

int n1 , n2 , n3 ;

n1 = 5 ;

n2 = ++n1 ; /* обе переменные - n1 и n2 - получают значение 6 */

n1 = 5 ;

n3 = n1++ ; /* n1 принимает значение 6, а n3 - 5 */

Другими словами, переменной n2 присваивается уже увеличенное префиксным оператором инкремента значение n1, тогда как переменной n3 передаётся ещё не увеличенное постфиксным оператором значение n1.

Почему так важен оператор инкремента...53

Разработчики С++ заметили, что программисты прибавляют 1 чаще, чем любую другую константу. Учитывая это, в язык была добавлена соответствующая конструкция.

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

_________________

53 стр. Глава 3. Выполнение математических операций

►Использование операторов присвоения...54

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

Создатели С++ заметили, что присвоение часто имеет вид

variable = variable # constant

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

nVariable = nVariable + 2 ;

Из этой записи следует, что к значению переменной nVariable добавляется двойка и результат снова сохраняется в nVariable.

«Использование в левой и правой части выражения одной и той же переменной — весьма распространённое явление в программировании.»

[Советы]

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

nVariable += 2 ;

Смысл этой записи таков: "значение переменной nVariable увеличено на 2".

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

[Советы]

_________________

54 стр. Часть 1. Первое знакомство с С++

Глава 4. ВЫПОЛНЕНИЕ ЛОГИЧЕСКИХ ОПЕРАЦИЙ...55

ОГЛАВЛЕНИЕ

В этой главе...

►Зачем нужны логические операторы 55

►Использование простых логических операторов 55

►Бинарные числа в С++ 60

►Выполнение побитовых логических операций 62

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

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

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

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

►Зачем нужны логические операторы...55

Программы на С++ должны уметь принимать решения. Программы, написанные без принятия решений, по сложности подобны приведённой в первой главе ( вспомните, что все выполняемые ею действия совершенно безальтернативны ). Программам очень часто приходится выполнять примерно следующее: "Сделай то, если переменная а меньше некоторого значения, а если не меньше — то сделай это". Для принятия решений ( правильных или нет ) в программах просто необходимо использовать логические операторы.

►Использование простых логических операторов...55

Простые логические операторы приведены в табл. 4.1. Они могут возвращать два значения: true ( истина ) и false ( ложь ).

_________________

55 стр. Глава 4. Выполнение логических операций

Таблица 4.1. Простые операторы из повседневной логики

_________________

Оператор — Значение

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

==  — ( Равенство; истинно, когда значение левого аргумента совпадает со значением правого )

!=  — ( Неравенство; противоположно равенству )

> , < — ( Больше, меньше; истинно, когда значение левого выражения больше ( или меньше ) значения правого )

>= , <=  — ( Больше или равно, меньше или равно; истинно, если истиной является > или == ( соответственно < или == ) )

&& —  ( И; истинно, если аргументы и слева и справа являются истиной )

|| — ( ИЛИ; истинно, если или левый, или правый аргумент являются истиной )

!  — ( НЕ; истинно, если его аргумент принимает ложное значение )

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

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

n == 0 ;

«Не перепутайте оператор равенства == ( двойное равно ) с оператором присвоения = ( одинарное равно ). Эта ошибка очень распространена, к тому же компилятор С++ вообще не считает её ошибкой, что делает её вдвойне опасной!»

[Атас!]

n = 0 ; /* Программист хотел написать, что n == 0 */

Широко распространены в повседневной жизни операторы "больше" ( > ) и "меньше" ( < ). Приведём пример логического выражения, возвращающего значение true.

int n1 = 1 ;

int n2 = 2 ;

n1 < n2

«Операторы "больше" и "меньше" внешне очень похожи, и поэтому их легко перепутать. Чтобы этого не случилось, помните, что оператор-стрелочка принимает значение true в том случае, когда из двух сравниваемых значений он указывает на меньшее.»

[Советы]

С помощью операторов > и < можно найти случаи, когда n1 больше или меньше n2, однако при этом игнорируется возможность равенства их значений. Операторы "больше или равно" ( >= ), "меньше или равно" ( <= ), в отличие от только что рассмотренных, учитывают и эту возможность.

Так же широко используются операторы && ( И ) и || ( ИЛИ ). Эти операторы обычно сочетаются с другими логическими операторами:

/* истинно, если n2 больше n1 и меньше n3 */

( n1 < n2 )&&( n2 < n3 ) ;

В качестве ремарки: оператор "больше или равно" можно определить как[ 8 ]

n1 <= n2 эквивалентно ( n1 < n2 ) || ( n1 == n2 )

______________

8В качестве ещё одной ремарки: операторы сравнения вообще достаточно взаимозаменяемы. Так, например, ( a == b ) эквивалентно ( ! ( a>b )&&! ( a<b ) ). — Прим. ред.

_________________

56 стр. Часть 1. Первое знакомство с С++

Хранение логических значений...57

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

int n1 = 1 ;

int n2 = 2 ;

bool b ;

b = ( n1 == n2 ) ;

Это выражение показывает разницу между операторами присвоения и сравнения. Его можно описать следующими словами: "Сравни содержимое переменных n1 и n2 и сохрани результат сравнения в переменной b".

«Оператор присвоения имеет наиболее низкий приоритет, поэтому оператор сравнения будет выполнен до присвоения. Скобки в этой ситуации излишни, и выражение b = n1 == n2 эквивалентно b = ( n1 == n2 ). Обратите также внимание на различие операторов присвоения и сравнения.»

[Советы]

Вот пример программы, демонстрирующей использование переменной типа bool:

/*  BoolTest — сравнение данных,  вводимых с клавиатуры, и сохранение результата  в переменной типа bool */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

int main ( int nNumberofArgs , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

cout << "Использование переменной типа bool\n" ;

/*  Устанавливаем текстовый формат для вывода логических значений */

cout.setf( cout.boolalpha ) ;

/*  Инициализируем аргументы */

int nArg1 ;

cout << "Введите значение 1: " ;

cin >> nArg1 ;

int nArg2 ;

cout << " Введите значение 2: " ;

cin >> nArg2 ;

bool b ;

b = nArg1 == nArg2 ;

cout << "Значение " << nArg1

<< " == "       << nArg2

<< " равно "     << b

<< endl ;

/*  Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ;

return 0 ;

}

Инструкция cout.setf( cout.boolalpha ) ; обеспечивает вывод логического значения в виде строки "true" или "false", а не в виде 1 или 0 , как принято по умолчанию.

_________________

57 стр. Глава 4. Выполнение логических операций

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

Использование переменной типа bool

Введите значение 1: 5

Введите значение 2: 5

Значение 5 == 5 равно true

Press any key to continue...

«Специальное значение endl вставляет символ перехода на новую строку. Разница между endl и описанным в главе 2, "Премудрости объявления переменных", символом '\n' поясняется в главе 24, "Использование потоков ввода-вывода".»

[Советы]

Использование целых переменных в качестве логических...58

С++ не всегда имел тип bool. Ранее для хранения логических значений в С++ использовался тип int. Значение 0 рассматривалось как false, а все остальные — как true. Все логические операторы генерировали целочисленные значения 0 и 1, соответствующие значениям false и true.

В С++ осталась высокая степень совместимости типов bool и int, обеспечивающая поддержку старых программ. Например, если в только что рассмотренной программе удалить инструкцию cout.setf( cout.boolalpha ) ;, вывод программы будет следующим:

Использование переменной типа bool

Введите значение 1: 5

Введите значение 2: 5

Значение 5 == 5 равно 1

Press any key to continue...

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

int n1 = 1 ;

int n2 = 2 ;

int n ;

n = ( n1 == n2 ) ;

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

Логические операции и действительные переменные...58

Переменные с плавающей точкой, как уже отмечалось, не могут использоваться для перечисления. Вы можете сказать: первый, второй, третий, четвёртый и т.д., так как соотношения между 1, 2, 3 абсолютно точно известны. Но нет никакого смысла говорить о номере 4.535887 в последовательности ( такой способ нумерации возможен лишь как обозначение чего-то между четвёртым и пятым, но не действительного значения номера, так как в любом сколь угодно малом отрезке их несчётное[ 9 ] множество ).

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

float f1 = 10.0 ;

float f2 = f1 / 3 ;

f1 == ( f2 * 3.0 ) ; /* Равны ли эти значения? */

__________________

9Более того, в данном случае это не красивое слово, а строгий математический термин. — Прим. ред.

_________________

58 стр. Часть 1. Первое знакомство с С++

Сравнивая начальное и полученное значения, мы не обязательно получим равенство. Действительные переменные, с которыми работает компьютер, не могут содержать бесконечное число значимых разрядов. Поэтому f2 равняется, например, 3.3333, а не 3^1/3. В отличие от математики, в компьютере число троек после точки ограничено. Умножив 3.3333 на 3, вы, вероятно, получите не 10.0, а 9.9999. Такой маленькой разницей может пренебречь человек, но не компьютер. Эта машина понимает под равенством исключительно точное равенство значений.

«Переменная типа float поддерживает точность около 6 значащих цифр, а double — 13. Я говорю "около", так как компьютер часто генерирует числа наподобие 3.3333347 из-за особенностей вычислений с плавающей точкой.»

[Технические подробности]

В "чистой" математике количество троек после десятичной точки бесконечно, но компьютер не в состоянии работать с бесконечными числами. Поэтому при умножении 3.3333 на 3 мы получим 9.9999, а не 10, которое должно получаться при умножении 3^1/3 на 3 — так называемая ошибка округления. Такие малые отличия двух чисел несущественны для человека, но не для компьютера. Равенство означает в точности точное равенство ( неплохой каламбур? ).

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

Проблемы могут появиться и при совершенно простых вычислениях, например:

float f1 = 10.0 ;

float f2 = 100 % 30 ;

f1 == f2 ; /* истинно ли это выражение? */

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

float f1 = 10.0 ;

float f2 = f1 / 3 ;

float f3 = f2 * 3.0 ;

( f1 - f3 ) < 0.0001 && ( f3 - f1 ) < 0.0001 ;

Оно истинно в том случае, если разница между f1 и f2 меньше какого-то малого значения ( в нашем случае — 0.0001 ); при этом небольшие погрешности вычислений на правильность сравнения не повлияют.

Сокращённые вычисления в С++...59

Рассмотрим следующую конструкцию:

условие1 && условие2

Если условие1 ложно, то результат не будет истинным, независимо от истинности выражения условие2. В схеме

условие1 || условие2

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

Для экономии времени С++ вычисляет первым условие1, и в случае, если оно ложно ( для оператора && ) или истинно ( для оператора || ), выражение условие2 не вычисляется и не анализируется.

_________________

59 стр. Глава 4. Выполнение логических операций

►Бинарные числа в С++...60

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

«Вряд ли вам придётся часто работать с переменными на битовом уровне, поэтому остальную часть главы следует рассматривать как техническое отступление от основного повествования.»

[Технические подробности]

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

Десятичная система счисления...60

Числа, которыми мы чаще всего пользуемся, называются десятичными, или числами по основанию 10. В большинстве случаев программисты на С++ тоже используют десятичные переменные. Например, мы говорим, что значение переменной var равно 123.

Число 123 можно представить в виде 1*100+2*10+3*1. При этом каждое из чисел 100, 10, 1 является степенью 10.

123 = 1 * 100 + 2 * 10 + 3 * 1,

что эквивалентно следующему:

123 = 1 * 10^2 + 2 * 10^1 + 3 * 10^0

Помните, что любое число в нулевой степени равняется 1.

Другие системы счисления...60

Использование числа 10 в качестве основания нашей системы счисления объясняется, по всей вероятности, тем, что издревле для подсчётов человек использовал пальцы рук. Учитывая особенности нашей анатомии, удобной альтернативной системой счисления можно было бы выбрать двадцатеричную[ 10 ] ( т.е. с основанием 20 ).

Если бы наша вычислительная система была заимствована у собак, то она была бы восьмеричной ( ещё один "разряд", находящийся на задней части каждой лапы, не учитывается ). Эта система счисления работала бы не менее хорошо:

123_10 = 1* 8^2 + 7* 8^1 + 3* 8^0 = 173_8

Индексы 10 и 8 указывают систему счисления: 10 — десятичная, 8 — восьмеричная. Основанием системы счисления может быть любое положительное число.

Двоичная система счисления...60

У компьютеров, видимо, пальцев поменьше ( может быть, поэтому они такие недалёкие? ). Они предпочитают пользоваться двоичной системой счисления. Число 123 переводится в двоичную систему таким образом:

123_10 = 0*2^7+ 1*2^6 + 1*2^5 + 1*2^4 + 1*2^3 + 0*2^2 + 1*2^1 + 1*2^0 = 0*128 + 1*64 + 1*32 + 1*16 + 1*8 + 0*4 + 1*2 + 1*1 = 01111011_2

______________________

10Что и было сделано у некоторых народов, например у майя или чукчей. — Прим. ред.

_________________

60 стр. Часть 1. Первое знакомство с С++

Существует соглашение, которое гласит, что в записи двоичных чисел используются 4, 8, 16, 32 или даже 64 двоичных цифр, даже если старшие цифры — нули. Внутреннее представление числа в компьютере строится именно таким образом.

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

Поскольку основа двоичной системы счисления очень мала, для представления чисел необходимо использовать слишком большое количество битов. Для представления таких обычных чисел, как 123_10, неудобно использовать выражения вида 01111011_2. Поэтому программисты предпочитают представлять числа блоками из четырёх битов.

С помощью одного четырёхбитового блока можно представить любое число от 0 до 15, и такая система счисления называется шестнадцатеричной ( hexadecimal ), т.е. системой по основанию 16. Часто употребляют её сокращённое название hex.

В шестнадцатеричной системе обозначения цифр от 0 до 9 остаются теми же, а числа от 10 до 15 представляются с помощью первых шести букв алфавита: А вместо 10, В вместо 11 и т.д. Следовательно, 123_10 — это 7В_16.

123 = 7 * 16^1 + В ( т.е. 11 ) * 16^0 = 7В_16

Поскольку программисты предпочитают представлять числа с помощью 4, 8, 16 или 32 битов, шестнадцатеричные числа состоят соответственно из 1, 2, 4 или 8 шестнадцатеричных разрядов ( даже если ведущие разряды равны 0 ).

В заключение замечу, что, так как терминал не поддерживает нижний индекс, записывать шестнадцатеричные символы в виде 7В_16 неудобно. Даже в том текстовом редакторе, который я использую сейчас, довольно неудобно всякий раз менять режимы шрифтов для ввода всего двух символов. Поэтому программисты договорились начинать шестнадцатеричные числа с 0х ( это странное обозначение было придумано ещё во время разработки языка С ). Таким образом, 7В_16 равно 0x7В. Следуя этому соглашению, 0x7В равно 123, тогда как 0x123 равно 291.

К шестнадцатеричным числам можно применять все те же математические операторы, что и к десятичным. Просто нам трудно выполнить в уме умножение чисел 0хС * 0хЕ потому, что таблица умножения, которую мы учили в школе, применима только к десятичной системе счисления.

Выражения с римскими числами...61

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

XIX + XXVI = XLV

Последовательность выполнения сложения такова:

а) IX+VI: I после V "уничтожает" I перед X, поэтому в результате получаем XV ;

б) X+XX=XXX, если добавить ещё один X, получим XXXX, или XL.

Сложность вычитания римских чисел приблизительно такая же. Однако чтобы умножить два римских числа, требуется диплом бакалавра математики ( у вас волосы станут дыбом от правила, которое объясняет, как добавить к X разряд справа так, чтобы X*IV равнялось XL ). А уж о делении римских чисел можно писать целые диссертации. Словом, хорошо, что мы пользуемся арабскими числами...

_________________

61 стр. Глава 4. Выполнение логических операций

►Выполнение побитовых логических операций...62

Все числа С++ могут быть представлены в двоичном виде, т.е. с использованием только 0 и 1 в записи числа. В табл. 4.2 указаны операции, которые работают с числами побитово; отсюда и происходит название термина "побитовые операции".

Таблица 4.2. Побитовые операции

_________________

Оператор — Функция

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

~  — Каждый бит меняет своё значение на противоположное: 0 заменяется 1, 1 — нулём

&  — Побитовое И: поочередно выполняет операцию И с парами битов левого и правого аргумента

|  — Побитовое ИЛИ

^  — Побитовое исключающее ИЛИ

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

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

Кроме того, побитовые операции выполняются крайне быстро. Хранение 32 характеристик в одном типе не приводит ни к каким дополнительным затратам.

«Хотя память становится всё дешевле, она всё равно не безгранична. Иногда, при работе с большими объёмами данных, такая экономия с использованием битовых представлений весьма существенна.»

[Советы]

Побитовые операции с одним битом...62

Побитовые операторы &, ^, | и ~ выполняют логические операции над отдельными битами числа. Если рассматривать 0 как false и 1 как true ( так принято, хотя можно ввести и другие обозначения ), то для оператора ~ справедливо следующее:

~1( true ) равно 0( false )

~0( false ) равно 1( true )

Оператор & определяется так:

1( true ) & 1 ( true ) равно 1( true )

1( true ) & 0 ( false ) равно 0( false )

0( false ) & 0 ( false ) равно 0( false )

0( false ) & 1 ( true ) равно 0( false )

Для оператора |:

1( true ) | 1 ( true ) равно 1( true )

1( true ) | 0 ( false ) равно 1( true )

0( false ) | 0 ( false ) равно 0( false )

0( false ) | 1 ( true ) равно 1( true )

_________________

62 стр. Часть 1. Первое знакомство с С++

Таблица 4.3. Таблица истинности оператора &

&  1 0

1   1 0

0   0 0

В таблице столбцы соответствуют значению одного аргумента, а строки — второго; результат операции находится на пересечении соответствующих строки и столбца. Так, из таблицы видно, что получить в результате операции & можно только если оба операнда равны 1. Далее в табл. 4.4 приведена таблица истинности оператора | ( ИЛИ ).

Таблица 4.4. Таблица истинности оператора |

|   1 0

1  1 1

0  1 0

Для последнего логического оператора, называемого "исключающее или" ( XOR ), прямой аналог в повседневной жизни найти труднее. Он возвращает значение true, если истинным является какой-то один ( но не оба! ) из его аргументов. Таблица истинности этого оператора представлена ниже ( табл. 4.5 ).

Таблица 4.5. Таблица истинности оператора ^

^  1 0

1  0 1

0  1 0

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

Использование побитовых операторов...63

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

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

~0110_2 ( 0х6 )

1001_2 ( 0x9 )

Таким образом получаем, что ~0x6 равно 0x9. В следующем примере продемонстрировано выполнение оператора &:

0110_2

&

0011_2

0010_2

Вычисляем, начиная со старших битов: 0 & 0 равно 0. В следующем бите 1 & 0 равно 0. В третьем бите 1 & 1 даёт 1, а в последнем бите 0 & 1 даёт 0.

_________________

63 стр. Глава 4. Выполнение логических операций

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

0x06        0110_2

&             &

0x03        0011_2

0x02        0010_2

Расписав числа таким образом, мы получили, что 0x6 & 0x3 равно 0x2. ( Попробуйте подсчитать значение выражения 0х6 | 0x3. Если вам это удастся, вы почувствуете себя на седьмом небе. Иначе очутитесь на первой из семи ступенек в преисподнюю. У меня на это ушло чуть меньше восьми минут. )

Простой пример...64

Следующая программа иллюстрирует работу побитовых операторов. В ней инициируются две переменные, к которым применяются операции &, |, ~, ^. Результаты вычислений выводятся на экран.

/* BitTest — инициируются две переменные */

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

/*            операторов |, ^, ~ и &  */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

int main( int nNumberofArgs , char* pszArgs[ ] )

{

/* отмена формата по умолчанию( десятичного ), или можно и так отменить cout.unsetf( cout.dec ) */

cout.unsetf( ios::dec ) ;

/* теперь можно установить вывод переменных в шестнадцатеричном виде */

/* установка вывода переменных в шестнадцатеричном виде, или можно и так cout.setf( cout.hex ) */

cout.setf( ios::hex ) ;

/* инициализация двух аргументов */

int nArg1 ;

nArg1 = 0x1234 ;

int nArg2 ;

nArg2 = 0x00ff ;

/* Выполнение логических операций */

/* Сначала применяем унарный оператор NOT */

cout << "Arg1          = 0x" << nArg1 << "\n" ;

cout << "Arg2          = 0x" << nArg2 << "\n" ;

cout << "~nArg1        = 0x" << ~nArg1 << "\n" ;

cout << "~nArg2        = 0x" << ~nArg2 << "\n" ;

/* Теперь — бинарные операторы */

cout << "nArg1 & nArg2 = 0x"

<< ( nArg1 & nArg2 )

<< "\n" ;

cout << "nArg1 | nArg2 = 0x"

<< ( nArg1 | nArg2 )

<< "\n" ;

cout << "nArg1 ^ nArg2 = 0x"

<< ( nArg1 ^ nArg2 )

<< "\n" ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ;

return 0 ;

}

_________________

64 стр. Часть 1. Первое знакомство с С++

Первая инструкция нашей программы ( та, которая следует за ключевым словом main ) — cout.setf( ios::hex ) ; — меняет используемый по умолчанию десятичный формат вывода на шестнадцатеричный ( поверьте мне, это сработает ).

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

Arg1                    = 0x1234

Arg2                    = 0xff

~nArg1                  = 0xffffedcb

~nArg2                  = 0xffffff00

nArg1 & nArg2           = 0x34

nArg1 | nArg2           = 0x12ff

nArg1 ^ nArg2           = 0x12cb

Press any key to continue...

Практическое применение логических вычислений...65

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

_________________

65 стр. Глава 4. Выполнение логических операций

Глава 5. ОПЕРАТОРЫ УПРАВЛЕНИЯ ПРОГРАММОЙ...66

ОГЛАВЛЕНИЕ

В этой главе...

►Управление ходом программы с помощью команд ветвления 66

►Выполнение циклов 68

►Вложенные команды управления 76

►Инструкция выбора 77

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

Например, если пользователь нажимает <Ctrl+C>, компьютер копирует содержимое выделенного блока в буфер обмена. Если пользователь перемещает мышь, на экране двигается её курсор. Этот список можно продолжать до бесконечности. Программы же, которые не принимают никаких решений, неизбежно скучны и однообразны.

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

►Управление ходом программы с помощью команд ветвления...66

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

if ( m > n )

{

/* 1-я последовательность операторов. Инструкции, которые должны быть выполнены, если m больше n */

}

else

{

/* 2-я последовательность операторов. Инструкции, которые нужно выполнить в противном случае */

}

_________________

66 стр. Часть 1. Первое знакомство с С++

Прежде всего вычисляется логическое выражение m > n. Если его значение — true, программа выполняет первую последовательность операторов. Если же выражение ложно, управление передаётся второй последовательности. Оператор else не обязателен: если он опущен, С++ считает, что он существует, но является пустым.

«Если в текущей ветви оператора if имеется только одна инструкция, скобки использовать необязательно. Однако очень легко сделать ошибку, которую без скобок, определяющих структуру операторов, компилятор С++ обнаружить не сможет.»

[Атас!]

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

Работу оператора if можно рассмотреть на следующем примере:

/* BranchDemo — введите два числа.  */

/*                   Если первый аргумент больше, выполняем операторы первой ветви, если меньше — второй. */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

int main( int argc , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */

/* вводим первый аргумент... */

int arg1 ;

cout << "Введите arg1: " ;

cin >> arg1 ;

/* ... второй */

int arg2 ;

cout << "Введите arg2: " ;

cin >> arg2 ;

/* теперь решаем, что делать: */

if ( arg1 > arg2 )

{

cout << "Аргумент 1 больше, чем аргумент 2"

<< endl ;

}

else

{

cout << "Аргумент 1 не больше, чем аргумент 2"

<< endl ;

}

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

__________________

67 стр. Глава 5. Операторы управления программой

Программа считывает два целых числа, вводимых с клавиатуры, и сравнивает их. Если выражение "arg1 больше arg2" истинно, то выполняется инструкция cout << "Аргумент 1 больше, чем аргумент 2 " ;. Если же нет, то управление переходит к последовательности операторов, соответствующей условию else: cout << "Аргумент 1 не больше , чем аргумент 2 " ;. Вот пример работы программы:

Введите arg1: 5

Введите arg2: 6

Аргумент 1 не больше, чем аргумент 2

Press any key to continue...

►Выполнение циклов...68

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

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

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

Цикл while...68

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

while ( условие )

{

/* Этот код выполняется повторно, пока условие остаётся истинно */

}

Сначала проверяется условие. Условием могут быть выражения вида var > 10 , var1 == var2 или любые другие. Если условие истинно, выполняются инструкции в скобках. Дойдя до закрывающей скобки, компилятор передаёт управление в начало цикла, и всё повторяется вновь. Таким образом, смысл оператора while в том, что программный код в скобках повторно выполняется до тех пор, пока не нарушится условие ( этот процесс напоминает мне утренние прогулки с собакой вокруг дома, пока она не... ну а потом мы возвращаемся ).

Если условие сначала было справедливо, тогда что может заставить его стать ложным? Рассмотрим следующий пример программы:

/* WhileDemo — введите счётчик цикла. Программа выводит количество выполненных циклов while */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

int main( int argc , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */

_________________

68 стр. Часть 1. Первое знакомство с С++

/* Ввод счётчика цикла */

int loopCount ;

cout << "Введите loopCount: " ;

cin >> loopCount ;

/* Теперь в цикле выводим значения */

while ( loopCount > 0 )

{

loopCount = loopCount - 1 ;

cout << "Осталось выполнить "

<< loopCount << " циклов( a )\n" ;

}

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

Программа WhileDemo получает от пользователя значение счётчика цикла, которое сохраняется в переменной loopCount. Затем программа выполняет цикл while. Сначала проверяется значение переменной loopCount. Если оно больше нуля, программа входит в тело цикла ( телом цикла называется код между скобками ), где loopCount уменьшается на 1, и результат выводится на экран. Затем программа возвращается к началу цикла и проверяет, осталась ли переменная loopCount положительной.

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

Введите loopCount: 5

Осталось выполнить 4 циклов( а )

Осталось выполнить 3 циклов( а )

Осталось выполнить 2 циклов( а )

Осталось выполнить 1 циклов( а )

Осталось выполнить 0 циклов( а )

Press any key to continue...

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

Реже используется другая версия цикла while, известная как do ... while. Она работает аналогично, но условие завершения проверяется в конце, после выполнения тела цикла.

do

{

// Тело цикла

}

while ( условие ) ;

Поскольку условие проверяется только в конце, тело оператора do ... while выполняется всегда хотя бы один раз.

«Условие завершения цикла проверяется только в начале оператора while или в конце оператора do . . . while. Даже если в какой-то момент оно перестанет быть справедливым, программа продолжит выполнение цикла до следующей проверки условия.»

[Атас!]

_________________

69 стр. Глава 5. Операторы управления программой

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

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

while ( loopCount > 0 )

{

loopCount = loopCount - 1 ;

cout << "Осталось выполнить "

<< loopCount << " циклов\n" ;

}

Используя оператор декремента, этот цикл можно записать более компактно:

/* В цикле выводим значения */

while ( loopCount > 0 )

{

loopCount-- ;

cout << "Осталось выполнить "

<< loopCount << " циклов\n" ;

}

Смысл этого варианта цикла полностью совпадает со смыслом оригинала. Единственная разница — в способе записи.

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

/* В цикле выводим значения */

while ( loopCount-- > 0 )

{

cout << "Осталось выполнить "

<< loopCount << " циклов\n" ;

}

Хотите — верьте, хотите — нет, но большинство программистов на С++ используют именно этот вариант записи. И не потому, что им нравится быть остроумными; хотя почему бы и нет? Использование в логических сравнениях операторов инкремента и декремента делает программный код легко читаемым и более компактным. И вряд ли вы, исходя из своего опыта, сможете предложить достойную альтернативу.

«И в выражении loopCount--, и в --loopCount значение loopCount уменьшается; однако первое возвращает значение переменной loopCount перед его уменьшением на 1, а второе — после.»

[Помни!]

Сколько раз будет выполняться декрементированный вариант WhileDemo, если пользователь введёт число 1? Если использовать префиксный вариант, то значение --loopCount равно 0 и тело цикла никогда не выполнится. В постфиксном варианте loopCount-- возвратит 1 и программа передаст управление в начало цикла.

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

_________________

70 стр. Часть 1. Первое знакомство с С++

Использование цикла for...71

Другой разновидностью циклов является цикл for. Его часто предпочитают более простому циклу while. Цикл for имеет следующий вид:

for ( инициализация ; условие ; увеличение )

{

// ...тело цикла

}

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

Затем проверяется условие. Подобно циклу while, цикл for выполняется до тех пор, пока условие не станет ложным.

После того как выполнится код тела цикла, управление получит следующий параметр цикла for ( увеличение[ 11 ]) и значение счётчика изменится. Затем опять будет выполнена проверка условия, и процесс повторится. В этом параметре обычно записывают инкрементное или декрементное выражение, которое определяет характер изменения переменной цикла на каждой итерации, но в принципе ограничений на используемые здесь операторы нет. Цикл for можно заменить эквивалентным ему циклом while:

инициализация ;

while ( условие )

{

{

// ...тело цикла

}

увеличение ;

}

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

Для лучшего понимания цикла for рассмотрим пример. Приведённая ниже программа ForDemo выполняет то же, что и WhileDemo, но вместо while использует цикл for.

/* ForDemo1. Вводится счётчик цикла. На экран выводится количество выполненных циклов for */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

int main( int nNumberofArgs , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */

/* Ввод счётчика цикла */

int loopCount ;

cout << "Введите loopCount: " ;

cin >> loopCount ;

/* Работаем loopCount раз */

for ( ; loopCount > 0 ; )

{

loopCount = loopCount - 1

cout << "Осталось выполнить "

<< loopCount << " циклов\n" ;

}

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

_______________

11Увеличение в данном случае — достаточно условное название. Чаще всего здесь действительно выполняется увеличение счётчика цикла, однако это может быть любая инструкция С++, в том числе и пустая. — Прим. ред.

_________________

71 стр. Глава 5. Операторы управления программой

Программа ForDemo1 считывает вводимое при помощи клавиатуры значение в переменную loopCount. Управление передаётся циклу, если введённое значение больше нуля. Внутри цикла программа уменьшает значение счётчика и выводит получившееся значение, после чего управление вновь передаётся оператору for. Как только значение loopCount становится равным 0 , работа цикла завершается.

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

[Советы]

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

/* ForDemo2 — Вводится счётчик цикла. На экран выводится количество выполненных циклов for */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

int main( int nNumberofArgs , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */

/* Ввод количества циклов */

int loopCount ;

cout << "Введите loopCount: " ;

cin >> loopCount ;

/* Цикл до достижения значения loopCount */

for ( int i = 1 ; i <= loopCount ; i++ )

{

cout << "Выполнено " << i << " циклов( a )\n" ;

}

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

_________________

72 стр. Часть 1. Первое знакомство с С++

Программа ForDemo2 выполняет те же действия, что и ранее рассмотренная WhileDemo. Однако вместо изменения переменной loopCount в этом варианте программы введена специальная переменная цикла.

Выполнение цикла начинается с объявления переменной i и инициализации её значением переменной loopCount. Затем проверяется, является ли переменная i положительной. Если переменная положительна, то программа выводит уменьшенное на 3 значение i и возвращается к началу цикла.

«Согласно последнему стандарту языка индексная переменная, объявленная в части инициализации цикла for, известна только в пределах этого цикла. Программисты на С++ в этом случае говорят, что область видимости переменной ограничена циклом for. Например, в инструкции return рассмотренного выше примера, т.е. за пределами цикла, переменная i недоступна и не может использоваться. Однако этого новейшего правила придерживаются далеко не все компиляторы, и вам нужно протестировать свой компилятор, чтобы узнать, как он действует в этом случае. Dev-C++ при использовании i вне цикла выводит предупреждение, но позволяет программисту поступить по своему усмотрению.»

[Атас!]

Избегайте бесконечных циклов...73

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

В следующей разновидности рассмотренного ранее цикла

while ( loopCount > 0 )

{

cout << "Осталось выполнить" << loopCount << " циклов\n"

}

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

«Но... ничто не вечно под Луной! В конечном счёте электричество отключится, компьютер поломается, Microsoft обанкротится... Или цикл прервётся, и вам не о чём будет тревожиться. Существует гораздо больше способов создания бесконечных циклов, чем показано здесь; но обычно они слишком сложны для того, чтобы приводить их в такой простой книжке.»

[Технические подробности]

Специальные операторы управления циклом...73

В С++ определены две специальные управляющие программой команды — break и continue. Может случиться, что условие работы цикла нарушится не в начале или в конце, а где-то посередине цикла. Рассмотрим программу, которая суммирует введённые пользователем значения. Цикл прерывается, когда пользователь вводит отрицательное число.

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

_________________

73 стр. Глава 5. Операторы управления программой

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

while ( условие )

{

if ( какое-то другое условие )

{

break ; /* выход из цикла */

}

}          /* когда программа встретит break, управление будет передано этой строке */

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

/* BreakDemo — вводим множество чисел. */

/*             Суммируем эти числа, пока пользователь не введёт отрицательное число */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

int main( int argc , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */

/* Введите счётчик цикла */

int accumulator = 0 ;

cout << "Эта программа суммирует числа, "

<< "введённые пользователем\n" ;

cout << "Выполнение цикла "

<< "заканчивается после "

<< "ввода отрицательного числа\n" ;

/* Бесконечный цикл */

for ( ; ; )

{

/* Ввод следующего числа */

int value = 0 ;

cout << "Введите следующее число: " ;

cin >> value ;

/* если оно отрицательно... */

if ( value < 0 )

{

/* ...тогда выходим из цикла */

break ;

}

/* ...иначе добавляем число к общей сумме */

accumulator = accumulator + value ;

}

/* После выхода из цикла выводим результат суммирования */

cout << "\nОбщая сумма равна"

<< accumulator

<< "\n" ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ;

return 0 ;

}

_________________

74 стр. Часть 1. Первое знакомство с С++

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

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

[Советы]

Эта программа суммирует числа, введённые пользователем

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

Введите следующее число: 1

Введите следующее число: 2

Введите следующее число: 3

Введите следующее число: -1

Общая сумма равна 6

Press any key to continue...

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

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

[Диск]

while ( 1 )

{

/* Ввод значения */

cout << "Введите значение" ;

cin >> inputVal ;

/* Если число отрицательное */

if ( inputVal < 0 )

{

/* Выводим сообщение об ошибке */

cout << "Не допускается ввод "

<< "отрицательных чисел\n" ;

/* Возвращаемся к началу цикла */

continue ;

}

/* Введено приемлемое значение */

}

_________________

75 стр. Глава 5. Операторы управления программой

►Вложенные команды управления...76

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

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

/* NestedDemo — вводится последовательность чисел. */

/*              Числа суммируются, пока пользователь не введёт отрицательное число. Этот процесс будет повторяться, пока общая сумма не станет равной 0. */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

int main( int argc , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */

cout << "Эта программа суммирует "

<< "последовательности чисел.\n Ввод каждой "

<< "последовательности завершается "

<< "вводом отрицательного числа \n"

<< "Чтобы завершить ввод последовательностей,"

<< "нужно ввести\nдва отрицательных числа подряд\n" ;

/* Внешний цикл работает с последовательностями чисел */

int accumulator ;

do

{

/* Начинаем ввод очередной последовательности чисел */

accumulator = 0 ;

cout << "\nВведите очередную последовательность\n" ;

/* Бесконечный цикл */

for ( ; ; )

{

/* Введение очередного числа */

int value = 0 ;

cout << "Введите очередное число: " ;

cin >> value ;

/* Если оно отрицательное... */

if ( value < 0 )

{

/* ...выходим из цикла */

_________________

76 стр. Часть 1. Первое знакомство с С++

break ;

}

/* ...иначе добавляем число к общей сумме */

accumulator = accumulator + value ;

}

/* Вывод результата вычислений... */

cout << "\nОбщая сумма равна "

<< accumulator

<<"\n" ;

/* ... если накопленная общая сумма чисел последовательности не равна нулю, начинаем работать со следующей последовательностью */

} while ( accumulator != 0 ) ;

cout << "Программа завершена\n" ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ;

return 0 ;

}

►Инструкция выбора...77

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

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

{

case c1 :

/* Переходим сюда, если выражение == c1 */

break ;

case c2 :

/* Переходим сюда, если выражение == c2 */

break ;

default :

/* Если ни одно условие не выполнено, переходим сюда */

}

Значением выражения должно быть целое число ( int, long или char ); c1, с2, с3 должны быть константами. Инструкция switch выполняется следующим образом: сначала вычисляется значение выражения, а затем оно сравнивается с константами, указанными после служебного слова case. Если константа соответствует значению выражения, то программа передаёт управление этой ветви. Если ни один вариант не подходит, выполняется условие default.

_________________

77 стр. Глава 5. Операторы управления программой

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

cout << "Введите 1, 2 или 3:" ;

cin >> choice ;

switch ( choice )

{

case 1 :

/* Обработка случая "1" */

break ;

case 2 :

/* Обработка случая "2" */

break ;

case 3 :

/* Обработка случая "3" */

break ;

default :

cout << "Вы ввели не 1, не 2 и не 3\n"

}

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

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

[Советы]

_________________

78 стр. Часть 1. Первое знакомство с С++

Часть 2. СТАНОВИМСЯ ФУНКЦИОНАЛЬНЫМИ ПРОГРАММИСТАМИ...79

ОГЛАВЛЕНИЕ

СОДЕРЖАНИЕ

Глава 6. СОЗДАНИЕ ФУНКЦИЙ...81

Глава 7. ХРАНЕНИЕ ПОСЛЕДОВАТЕЛЬНОСТЕЙ В МАССИВАХ...92

Глава 8. ПЕРВОЕ ЗНАКОМСТВО С УКАЗАТЕЛЯМИ В С++...105

Глава 9. ВТОРОЕ ЗНАКОМСТВО С УКАЗАТЕЛЯМИ...117

Глава 10. ОТЛАДКА ПРОГРАММ НА С++...128

В этой части...

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

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

_________________

80 стр. Часть 2. Становимся функциональными программистами

Глава 6. СОЗДАНИЕ ФУНКЦИЙ...81

ОГЛАВЛЕНИЕ

В этой главе...

►Написание и использование функций 81

►Подробный анализ функций  84

►Перегрузка функций 88

►Определение прототипов функций 89

►Хранение переменных в памяти 90

►Использование заголовочных файлов 91

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

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

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

►Написание и использование функций...81

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

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

В программе FunctionDemo показано, как упростить программу NestDemo с помощью создания функции sumSequence( ).

_________________

81 стр. Глава 6. Создание функций

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

[Советы]

/* FunctionDemo — демонстрация использования функций. */

/*               Внутренний цикл программы оформлен как отдельная функция */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

/* sumSequence — суммирует последовательность чисел, введённых     с клавиатуры, пока пользователь не введёт отрицательное число. Возвращает сумму введённых чисел */

int sumSequence( void )

{

/* Бесконечный цикл */

int accumulator = 0 ;

for ( ; ; )

{

/* Ввод следующего числа */

int value = 0 ;

cout << "Введите следующее число: " ;

cin >> value ;

/* Если оно отрицательное... */

if ( value < 0 )

{

/* ...тогда выходим из цикла */

break ;

}

/* ...иначе добавляем число к переменной accumulator */

accumulator = accumulator + value ;

}

/* Возвращаем значение суммы */

return accumulator ;

}

int main( int argc , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ". 1251" ) ; /* печать русских текстов */

cout << "Эта программа суммирует последовательности "

<< "чисел. Каждая\nпоследовательность"

<< "заканчивается отрицательным числом.\n"

<< "Ввод серий завершается вводом "

<< "двух отрицательных чисел подряд\n" ;

/* Суммируем последовательности чисел... */

int accumulatedValue ;

_________________

82 стр. Часть 2. Становимся функциональными программистами

for ( ; ; )

{

/* Суммируем последовательности чисел, введённых с клавиатуры */

cout << "\nВведите следующую последовательность\n" ;

accumulatedValue = sumSequence( ) ;

if ( accumulatedValue == 0 ) { break ; }

/* Вывод общей суммы на экран */

cout << "\nОбщая сумма равна "

<< accumulatedValue

<< "\n" ;

} ;

cout << "Программа завершена\n" ;

/*  Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

Определение функции sumSequence( )...83

Определение этой функции начинается с инструкции int sumSequence( void ). Заключённый в фигурные скобки блок кода называется телом функции. Как видите, тело функции sumSequence( ) идентично внутреннему циклу программы NestDemo.

Вызов функции sumSequence( )...83

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

Различие состоит в том, что внутри функции main( ) содержится выражение accumulatedValue = sumSequence( ). В правой части этого выражения вызывается функция sumSequence( ). Возвращаемое функцией значение сохраняется в переменной accumulatedValue, а затем выводится на экран. Главная программа выполняет цикл до тех пор, пока значение суммы, возвращаемой внутренней функцией, остаётся отличным от 0. Нулевое значение говорит о том, что пользователь закончил вычисление сумм последовательностей.

Разделяй и властвуй...83

Программа FunctionDemo выделяет внутренний цикл в функцию sumSequence( ). Такое выделение отнюдь не произвольно. Функция sumSequence( ) играет свою, отдельную роль.

«Хорошую функцию можно легко описать одним предложением с минимальным количеством слов "и" и "или". Например, функция sumSequence( ) суммирует последовательность целочисленных значений, введённых пользователем. Это определение весьма компактно и легко воспринимается.»

[Советы]

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

_________________

83 стр. Глава 6. Создание функций

Вот как выглядит пример работы рассмотренной программы.

Эта программа суммирует последовательности чисел. Каждая

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

Ввод серий завершается вводом двух отрицательных чисел подряд

Введите следующую последовательность

Введите следующее число: 1

Введите следующее число: 2

Введите следующее число: 3

Введите следующее число: -1

Общая сумма равна 6

Введите следующую последовательность

Введите следующее число: 1

Введите следующее число: 2

Введите следующее число: -1

Общая сумма равна 3

Введите следующую последовательность

Введите следующее число: -1

Программа завершена

Press any key to continue...

►Подробный анализ функций...84

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

Функцией называют логически обособленный блок кода С++, имеющий следующий вид:

< тип возвращаемого значения > name( < аргументы функции > )

{

// . . .

return < выражение > ;

}

Аргументами функции называются значения, которые можно передать ей при вызове. В возвращаемом значении указывается результат, который функция возвращает по окончании работы. Например, в вызове функции возведения в квадрат square ( 10 ) 10 — это аргумент, а возвращаемое значение равно 100.

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

В программе FunctionDemo используется функция с именем sumSequence( ), которая не имеет аргументов и возвращает значение целочисленного типа.

«Тип аргументов функции по умолчанию — void, поэтому функцию int fn( void ) можно записать как int fn( ).»

[Советы]

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

_________________

84 стр. Часть 2. Становимся функциональными программистами

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

Простые функции...85

Функция sumSequence( ) возвращает целое значение. Функции могут возвращать значение любого стандартного типа, например double или char ( типы переменных рассматриваются в главе 2, "Премудрости объявления переменных" ).

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

«Функции различаются по типу возвращаемого значения. Так, целочисленной функцией называют ту, которая возвращает целое значение. Функция, которая ничего не возвращает, известна как void-функция. Далее приведён пример функции, выполняющей некоторые действия, но не возвращающей никаких значений.»

[Советы]

void echoSquare( )

{

cout << "Введите значение:" ;

cin >> value ;

cout << " \n" << value*value << "\n" ;

return ;

}

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

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

[Советы]

Функции с аргументами...85

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

Функции с одним аргументом...85

Аргументами функции называют значения, которые передаются функции во время вызова. В следующем примере определяется и используется функция square( ), которая возвращает квадрат переданного в качестве аргумента числа типа double:

/* SquareDemo — демонстрирует использование функции с аргументом */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

_________________

85 стр. Глава 6. Создание функций

/* square — возвращает квадрат аргумента doubleVar — введённое значение return — квадрат doubleVar */

double square( double doubleVar )

{

return doubleVar * doubleVar ;

}

/* sumSequence — суммирует последовательность чисел, введённых с клавиатуры и возведённых в квадрат, пока пользователь не введёт отрицательное число. Возвращает сумму квадратов введённых чисел */

double sumSequence( void )

{

/* Бесконечный цикл */

double accumulator=0.0 ;

for ( ; ; )

{

/* Ввод следующего числа */

double dValue = 0 ;

cout << "Введите следующее число: " ;

cin  >> dValue ;

/* Если оно отрицательное... */

if ( dValue < 0 )

{

/* ...то выходим из цикла */

break ;

}

/* ...иначе вычисляем квадрат числа */

double value = square( dValue ) ;

/* Теперь добавляем квадрат к accumulator */

accumulator = accumulator + value ;

}

/* Возвращаем сумму */

return accumulator ;

}

int main( int nNumberofArgs , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */

cout << "Эта программа суммирует "

<< "несколько последовательностей чисел.\n"

<< "Ввод каждой последовательности "

<< "заканчивается\nвводом "

<< "отрицательного числа. \n"

<< "Последовательности вводятся "

<< "до тех пор, пока\nне встретятся "

<< "два отрицательных числа\n" ;

/* Продолжаем суммировать числа... */

_________________

86 стр. Часть 2. Становимся функциональными программистами

double accumulatedValue ;

for ( ; ; )

{

/* Суммируем последовательность чисел, введённых с клавиатуры */

cout << " \nВведите следующую последовательность\n" ;

accumulatedValue = sumSequence( ) ;

/* Выход из цикла */

if ( accumulatedValue <= 0.0 ) { break ; }

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

cout << "\nОбщая сумма равна "

<< accumulatedValue

<<" \n" ;

}

cout << "Программа завершена\n" ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

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

Функции с несколькими аргументами...87

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

int product( int arq1 , int arg2 )

{

return arg1 * arg2 ;

}

Функция main( )...87

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

При компиляции программы С++ добавляет некоторый стандартный программный код, выполняемый до того, как начинает выполняться функция main( ). Этот код настраивает программную среду, в которой выполняется ваша программа, например открывает потоки ввода и вывода по умолчанию.

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

_________________

87 стр. Глава 6. Создание функций

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

С++ позволяет программистам называть несколько разных функций одним и тем же именем. Эта возможность называется перегрузкой функций ( function overloading ).

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

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

void someFunction( void )

{

/* ...Выполнение некоторой функции */

}

void someFunction( int n )

{

/* ...Выполнение другой функции */

}

void someFunction( double d )

{

/* ...Выполнение ещё одной функции */

}

void someFunction( int n1 , int n2 )

{

/* ...Выполнение ещё одной функции, отличной от предыдущих */

}

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

Вы знаете, что тип аргумента void указывать не обязательно. Например, SumFunction( void ) и SumFunction( ) вызывают одну и ту же функцию. Фактически функция может иметь сокращённое название, в нашем случае someFunction( ), так же как и меня можно называть просто Стефаном. Если бы Стефанов больше нигде не было, меня всегда могли бы называть только по имени. Но в действительности нас побольше, и если кому-то посчастливится попасть в общество нескольких Стефанов, то к ним придётся обращаться по имени и фамилии ( кстати, напомню, что меня зовут Стефан Дэвис ). Пользуясь полным именем, никто не ошибётся, даже если Стефанов будет несколько. Поэтому, пока полные имена функций в программе уникальны, конфликты между ними невозможны.

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

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

int intVariable1 , intVariable2 ;

double doubleVariable ;

/* Функции различаются по типу передаваемых аргументов */

someFunction( ) ; /* Вызов someFunction( void ) */

someFunction( intVariable1 ) ; /* Вызов someFunction( int ) */

someFunction( doubleVariable ) ; /* Вызов someFunction( double ) */

someFunction( intVariable1 , intVariable2 ) ; /* Вызов someFunction( int , int ) */

/* С константами функции работают аналогично */

someFunction( 1 ) ; /* Вызов someFunction( int )*/

someFunction( 1.0 ) ; /* Вызов someFunction( double ) */

someFunction( 1 , 2 ) ; /* Вызов someFunction( int , int ) */

_________________

88 стр. Часть 2. Становимся функциональными программистами

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

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

int someFunction( int n ) ;

/* Полным именем этой функции является someFunction( int ) */

double someFunction( int n ) ; /* Имеет то же полное имя */

»

[Атас!]

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

int someFunction( int n ) ;

double d = someFunction( 10 ) ;

/* Преобразуем тип полученного значения */

»

[Помни!]

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

int someFunction( int n ) ;

double someFunction( int n ) ;

double d = someFunction( 10 ) ;

/* В этом случае мы преобразуем тип полученного целочисленного значения или используем вторую функцию? */

В этом случае С++ не поймёт, какое значение он должен использовать — возвращаемое double-функцией или её целочисленным вариантом. Поэтому такие функции в одной программе использоваться не могут.

►Определение прототипов функций...89

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

Функции sumSequence( ) и square( ), с которыми вы встречались в этой главе, были определены до того, как вызывались. Но это не означает, что нужно всегда придерживаться именно такого порядка. Функция может быть определена в любой части модуля ( модуль — это другое название исходного файла С++ ).

Однако должен использоваться какой-то механизм, уведомляющий функцию main( ) о функциях, которые она может вызывать. Рассмотрим следующий код:

int main( int argc , char* pArgs[ ] )

{

someFunc( 1 , 2 ) ;

}

int someFunc( double arg1 , int arg2 )

{

/* ...выполнение каких-то действий */

}

_________________

89 стр. Глава 6. Создание функций

При вызове функции someFunc( ) внутри main( ) полное её имя неизвестно. Можно предположить, что именем функции является someFunc( int , int ) и возвращаемое ею значение имеет тип void. Однако, как видите, это вовсе не так.

Согласен, компилятор С++ мог бы быть не таким ленивым и просмотреть весь модуль для определения сигнатуры функции. Но он этого не сделает, и с этим приходится считаться[ 12 ]. Таков мир: любишь кататься — люби и саночки возить.

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

Прототип функции содержит её полное имя с указанием типа возвращаемого значения. Использование прототипов рассмотрим на следующем примере:

int someFunc( double , int ) ;

int main( int argc , char* pArgs[ ] )

{

someFunc( 1 , 2 ) ;

}

int someFunc( double arg1 , int arg2 )

{

/* ...выполнение каких-то действий */

}

Использованный прототип объясняет миру ( по крайней мере той его части, которая следует после этого объявления ), что полным именем функции someFunc( ) является someFunc( double , int ). Теперь при её вызове в main( ) компилятор поймёт, что 1 нужно преобразовать к типу double. Кроме того, функция main( ) осведомлена, что someFunc( ) возвращает целое значение.

►Хранение переменных в памяти...90

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

int globalVariable ;

void fn( )

{

int localVariable ;

static int staticVariable ;

}

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

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

Статическая переменная staticVariable является чем-то средним между локальной и глобальной переменными. Она создаётся, когда программа при выполнении достигает описания переменной ( грубо говоря, когда происходит первый вызов функции ). К тому же staticVariable доступна только из функции fn( ). Но, в отличие от localVariable, переменная staticVariable продолжает существовать и после окончания работы функции. Если в функции fn( ) переменной staticVariable присваивается какое-то значение, то оно сохранится до следующего вызова fn( ).

________________

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

_________________

90 стр. Часть 2. Становимся функциональными программистами

►Использование заголовочных файлов...91

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

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

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

/* Функция abs возвращает абсолютное значение аргумента */

double abs( double d ) ;

/* Функция square возвращает квадрат аргумента */

double square( double d ) ;

Программа использует заголовочный файл math следующим образом:

/* Программа с математическими вычислениями */

#include "math"

using namespace std ;

// Код программы

Директива #include требует от препроцессора заменить её содержимым указанного в ней файла.( Между # и include можно ставить пробел, а вот между < и iostream нельзя.— Прим. рер. )

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

С++ предоставляет программисту стандартные заголовочные файлы, такие как cstdio или iostream. В частности, в файле iostream содержится прототип использованной в главе 4, "Выполнение логических операций", функции setf( ) для вывода чисел в шестнадцатеричном виде.

«Так сложилось, что годами программисты использовали расширение .h для заголовочных файлов. Однако в последние годы это соглашение для заголовочных файлов стандартной библиотеки С++ было отменено стандартом ( например, заголовочный файл cstdio ранее назывался stdio.h ). Однако многие программисты продолжают давать расширение .h своим заголовочным файлам. Даже в программировании есть традиции!»

[Атас!]

_________________

91 стр. Глава 6. Создание функций

Глава 7. ХРАНЕНИЕ ПОСЛЕДОВАТЕЛЬНОСТЕЙ В МАССИВАХ...92

ОГЛАВЛЕНИЕ

В этой главе...

►Преимущества массивов 92

►Использование символьных массивов 98

►Управление строками 100

►Тип string 103

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

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

►Преимущества массивов...92

Рассмотрим следующую проблему. Вам нужна программа, которая сможет считывать последовательность чисел, введённых с клавиатуры. Будем использовать уже привычное правило, согласно которому ввод чисел завершается после первого отрицательного значения. Однако данная программа, в отличие от уже рассмотренных в главах 5, "Операторы управления программой", и 6, "Создание функций", после того, как все числа прочитаны, отображает их на стандартном устройстве вывода.

Можно попытаться хранить числа в независимых переменных, например:

cin >> value1 ;

if ( value1 >= 0 )

{

cin >> value2 ;

if ( value2 >= 0 )

{

...

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

_________________

92 стр. Часть 2. Становимся функциональными программистами

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

int value ;

/* объявление массива, способного содержать до 128 чисел типа int */

int valueArray[ 128 ] ;

/* определение индекса, используемого для доступа к элементам массива; его значение не должно превышать 128 */

for ( int i = 0 ; i < 128 ; i++ )

{

cin >> value ;

/* выходим из цикла, если пользователь вводит отрицательное число */

if ( value < 0 ) break ;

valueArray[ i ] = value ;

}

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

Компьютер считывает число с клавиатуры и сохраняет его в следующем элементе массива valueArray. Доступ к элементам массива обеспечивается с помощью имени массива и индекса, указанного в квадратных скобках. Первый элемент массива обозначается как valueArray[ 0 ], второй — как valueArray[ 1 ] и т.д.

Запись valueArray[ i ] представляет собой i-й элемент массива. Индексная переменная i должна быть перечислимой, т.е. её типом может быть char , int или long. Если valueArray — массив целых чисел, то элемент valueArray[ i ] имеет тип int.

Работа с массивами...93

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

/* ArrayDemo — демонстрирует использование массивов. Считывает последовательность целых чисел и отображает их по порядку */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

/* объявления прототипов функций */

int sumArray( int integerArray[ ] , int sizeOfloatArray ) ;

void displayArray( int integerArray[ ] , int sizeOfloatArray ) ;

_________________

93 стр. Глава 7. Хранение последовательностей в массивах

int main( int nArg , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */

/* Описываем счётчик цикла */

int nAccumulator = 0 ;

cout << "Эта программа суммирует числа,"

<< " введённые пользователем\n" ;

cout << "Цикл прерывается, когда"

<< " пользователь вводит\n"

<< "отрицательное число\n" ;

/* Сохраняем числа в массиве */

int inputValues[ 128 ] ;

int numberOfValues = 0 ;

for ( numberOfValues = 0 ; numberOfValues < 128 ; numberOfValues++ )

{

/* Ввод очередного числа */

int integerValue ;

cout << "Введите следующее число: " ;

cin >> integerValue ;

/* Если оно отрицательное... */

if ( integerValue < 0 )

{

/* ...тогда выходим из цикла */

break ;

}

/* ...иначе сохраняем число в массиве */

inputValues[ numberOfValues ] = integerValue ;

}

/* Теперь выводим значения и их сумму */

displayArray( inputValues , numberOfValues ) ;

cout << "Сумма введённых чисел равна "

<< sumArray( inputValues , numberOfValues )

<< endl ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

/* displayArray — отображает элементы массива integerArray длиной sizeOfloatArray */

void displayArray( int integerArray[ ] , int sizeOfArray )

{

cout << "В массиве хранятся"

<< " следующие значения:\n" ;

for ( int i = 0 ; i < sizeOfArray ; i++ )

{

cout.width( 3 ) ;

cout << i << ": " << integerArray[ i ] << endl ;

}

cout << endl ;

}

_________________

94 стр. Часть 2. Становимся функциональными программистами

/* sumArray — возвращает сумму элементов целочисленного массива */

int sumArray( int integerArray[ ] , int sizeOfArray )

{

int accumulator = 0 ;

for ( int i = 0 ; i < sizeOfArray ; i++ )

{

accumulator += integerArray[ i ] ;

}

return accumulator ;

}

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

Если введённое значение отрицательно, цикл прерывается при помощи инструкции break, если же нет — оно копируется в массив. Целочисленная переменная numberOfValues используется в качестве индекса массива. Она инициализирована нулём в начале цикла for. При каждой итерации индекс увеличивается. В условии выполнения цикла for осуществляется контроль за тем, чтобы количество введённых чисел не превышало 128, т.е. размера массива ( после введения 128 чисел программа переходит к выводу элементов массива на экран независимо от того, ввёл пользователь отрицательное число или нет ).

«В объявлении массива inputValues было указано, что его максимальная длина равна 128. При записи большего числа данных, чем определено в объявлении, ваша программа может работать неправильно и даже аварийно завершать работу. Поэтому лучше застраховаться и оставить больше места для хранения данных. Неважно, насколько велик массив; всегда нужно следить за тем, чтобы операции с массивом не приводили к выходу за его пределы.»

[Атас!]

Функция main заканчивается выводом на экран содержимого массива и суммы его элементов.

«Среда Dev-C++ может помочь вам в работе с исходными текстами, в которых имеется много функций. На рис. 7.1 показано содержимое вкладки Classes ( Классы ), в которой перечисляются все функции в исходном файле. Двойной щелчок на имени функции переносит вас в окне редактирования в строку с этой функцией.»

[Советы]

Функция displayArray( ) содержит обычный цикл for, который используется для прохождения по массиву. Каждый очередной элемент массива добавляется к переменной accumulator. Передаваемый функции параметр sizeOfArray включает количество значений, содержащихся в массиве.

Напомню ещё раз, что индекс массива в С++ отсчитывается от 0 , а не от 1. Кроме того, обратите внимание, что цикл for прерывается в тот момент, когда значение i становится равным sizeOfArray. Вы же не хотите добавлять все 128 элементов массива integerArray к accumulator? Ни один элемент массива, индекс которого больше или равен числу sizeOfArray, учитываться не будет. Вот как выглядит пример работы с этой программой:

_________________

95 стр. Глава 7. Хранение последовательностей в массивах

Эта программа суммирует числа, введённые пользователем

Цикл прерывается, когда пользователь вводит

отрицательное число

Введите следующее число: 1

Введите следующее число: 2

Введите следующее число: 3

Введите следующее число: -1

В массиве хранятся следующие значения:

0: 1

1: 2

2: 3

Сумма введённых чисел равна 6

Press any key to continue...

Рис. 7.1. Вкладка Классы выводит информацию о функциях, составляющих программу

Инициализация массива...96

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

К счастью, массив может быть инициализирован сразу во время объявления, например:

float floatArray[ 5 ] = { 0.0 , 1.0 , 2.0 , 3.0 , 4.0 } ;

В этом фрагменте элементу floatArray[ 0 ] присваивается значение 0 , floatArray[ 1 ] — 1 , floatArray[ 2 ] — 2 и т.д.

_________________

96 стр. Часть 2. Становимся функциональными программистами

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

float floatArray[ ] = { 0.0 , 1.0 , 2.0 , 3.0 , 4.0 } ;

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

float floatArray[ 25 ] = { 1.0 } ;

Выход за границы массива...97

Математики перечисляют содержимое массивов, начиная с элемента номер 1. Первым элементом математического массива х является х( 1 ). Во многих языках программирования также начинают перечисление элементов массива с 1. Но в С++ массивы индексируются начиная с 0! Первый элемент массива С++ обозначается как valueArray[ 0 ]. Первый индекс массива С++ нулевой; поэтому последним элементом 128-элементного целочисленного массива является integerArray[ 127 ], а не integerArray[ 128 ].

К сожалению, в С++ не проверяется выход индекса за пределы массива. Этот язык будет рад предоставить вам доступ к элементу integerArray[ 200 ]. Более того, С++ позволит вам обратиться даже к integerArray[ -15 ]. Приведём такую аналогию. Имеется улица, на которой 128 жилых домов. Если мы захотим найти 200-й дом, идя вдоль улицы и пересчитывая дома, то его просто может не быть. Тут могут быть заброшенные руины или, хуже того, дом, стоящий уже на другой улице! Чтение значения элемента integerArray[ 200 ] может дать некоторое непредсказуемое значение или даже привести к ошибке нарушения защиты ( вы вторглись в частные владения, куда вас не звали... ), а запись — к совершенно непредсказуемым результатам. Может, ничего и не случится — вы просто попадёте в заброшенный дом, а может, вы сотрёте тем самым какие-то жизненно важные данные. Словом, случиться может что угодно — вплоть до полного краха программы.

«Самая распространённая ошибка — неправильное обращение к последнему элементу по адресу integerArray[ 128 ]. Хотя это всего лишь следующий за концом массива элемент, записывать или считывать его не менее опасно, чем любой другой некорректный адрес.»

[Атас!]

Использовать ли массивы...97

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

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

_________________

97 стр. Глава 7. Хранение последовательностей в массивах

Определение и использование массивов с элементами-массивами...98

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

В С++ матрицы определяются следующим образом:

int intMatrix[ 10 ][ 5 ] ;

Эта матрица может иметь 10 элементов в одном измерении и 5 в другом, что в сумме составляет 50 элементов. Другими словами, intMatrix является 10-элементным массивом, каждый элемент которого — массив из 5 элементов. Легко догадаться, что один угол матрицы обозначается intMatrix[ 0 ][ 0 ], тогда как второй — intMatrix[ 9 ][ 4 ].

Индексы intMatrix можно рассматривать в любом удобном порядке. По какой оси отложить длину 10 — решайте сами, исходя из удобства представления. Матрицу можно инициализировать так же, как и массив:

int intMatrix[ 2 ][ 3 ] = { { 1 , 2 , 3 } , { 4 , 5 , 6 } } ;

Здесь фактически выполняется инициализация двух трёхэлементных массивов: intMatrix[ 0 ] — значениями 1, 2 и 3, a intMatrix[ 1 ] — 4, 5 и 6 соответственно.

►Использование символьных массивов...98

Элементы массива могут быть любого типа. В С++ возможны массивы любых числовых типов — float, double, long, однако символьные массивы имеют особое значение.

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

char sMyName[ ] = { 'S' , ' t' , 'e' , 'p' , 'h' , 'e' , 'n' } ;

Моё имя можно отобразить с помощью следующей небольшой программы:

/* CharDisplay — выводит на экран массив символов в окне MS DOS */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

/* Объявления прототипов */

void displayCharArray( char stringArray[ ] , int sizeOfloatArray ) ;

int main( int nArg , char* pszArgs[ ] )

{

char charMyName[ ] = { 'S' , 't' , 'e' , 'p' , 'h' , 'e' , ' n' } ;

displayCharArray( charMyName , 7 ) ;

cout << endl ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ;

return 0 ;

}

_________________

98 стр. Часть 2. Становимся функциональными программистами

/* displayCharArray — отображает массив символов, по одному при каждой итерации */

void displayCharArray( char stringArray[ ] , int sizeOfloatArray )

{

for ( int i = 0 ; i < sizeOfloatArray ; i++ )

{

cout << stringArray[ i ] ;

}

}

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

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

Создание строки символов...99

В С++ для этой цели зарезервирован нулевой символ. Мы можем использовать '\0' для того, чтобы пометить конец символьного массива. ( Числовое значение '\0' равно нулю, однако тип '\0' — char. )

«Символ \у является символом, числовое значение которого равно у. Изменим предыдущую программу, используя это правило:»

[Помни!]

/* DisplayString — выводит на экран массив символов в окне MS DOS */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

/* Объявления прототипов */

void displayString( char stringArray[ ] ) ;

int main( int nArg , char* pszArgs[ ] )

{

char charMyName[ ] ={ 'S' , 't' , 'e' , 'p' , 'h' , 'e' , 'n' , 0 } ;

displayString( charMyName ) ;

cout << endl ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

_________________

99 стр. Глава 7. Хранение последовательностей в массивах

/* displayString — посимвольно выводит на экран строку */

void displayString( char stringArray[ ] )

{

for ( int i = 0 ; stringArray[ i ] != 0 ; i++ )

{

cout << stringArray[ i ] ;

}

}

Массив charMyName объявляется как массив символов с дополнительным нулевым символом ( \0 ) в конце. Программа displayString итеративно проходит по символьному массиву, пока не встретит нуль-символ.

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

«Строка символов — это символьный массив с завершающим нулевым символом. Зачастую его называют просто “строкой”, хотя в С++ имеется отдельный тип string для работы со строками.»

[Помни!]

Выбор нулевого символа в качестве завершающего не был случаен. Это связано с тем, что в С++ только нулевое значение преобразуется в логическое значение false, а все остальные — в true. Это означает, что цикл for можно записать ( что обычно и делается ) следующим образом:

for ( int i = 0 ; stringArray[ i ] ; i++ )

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

char szMyName[ ] = "Stephen" ;

char szAlsoMyName[ ] = { 'S' , 't' , 'e' , 'p' , 'h' , 'e' , 'n' , '\0' } ;

В соглашении об использовании имён для обозначения строк с завершающим нулём рекомендуется применять префикс sz. Такая запись является соглашением и не более.

«Строка "Stephen" содержит восемь, а не семь символов — не забывайте о нулевом символе!»

[Помни!]

►Управление строками...100

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

_________________

100 стр. Часть 2. Становимся функциональными программистами

Таблица 7.1. Функции, обрабатывающие строки

_________________

Название — Действие

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

int strlen( string ) — Возвращает количество символов в строке ( без учёта нулевого символа )

char* strcat( target , source ) — Присоединяет строку source к концу строки target

char* strcpy( target , source ) — Копирует строку source в target

char* strncat( target , source , n ) — Присоединяет не более n символов строки source к концу строки target

char* strncpy( target , source , n ) — Копирует не более n символов строки source в target

char* strstr( source1 , source2 ) — Находит первое вхождение строки source2 в source1

int strcmp( source1 , source2 ) — Сравнивает две строки

int stricmp( source1 , source2 ) — Сравнивает две строки без учёта регистра символов

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

Чтобы использовать функции работы со строками, нужно добавить в начале программы директиву #include <strings.h>.

«Текущий стандарт С++ предлагает избегать использования функций str...( ). В настоящее время компиляторы С++ поддерживают эти функции, но в один прекрасный день могут и перестать это делать. Именно с тем, что это устаревшие функции, связано использование расширения .h в директиве #include <strings.h>. Стандарт рекомендует использовать для работы с символьными строками специальный тип string.»

[Атас!]

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

/* Concatenate — объединение двух строк, которые разделяются символом " — " */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

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

#include <strings.h>

int main( int nArg , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */

/* Считываем первую строку... */

char szString1[ 256 ] ;

cout << "Введите строку #1: " ;

cin >> szString1 ;

/* Более безопасный вариант: cin.getline( szString1 , 128 ) ; */

/* ...теперь вторую... */

char szString2[ 128 ] ;

cout << "Введите строку #2: " ;

cin >> szString2 ;

/* Более безопасный вариант: cin.getline( szString2 , 128 ) ; */

_________________

101 стр. Глава 7. Хранение последовательностей в массивах

/* Объединяем строки */

char szString[ 260 ] ;

/* Копируем первую строку в буфер */

strncpy( szString , szString1 , 128 ) ;

/* Добавляем разделитель */

strncat( szString , " — " , 4 ) ;

/* ...теперь добавим вторую строку... */

strncat( szString , szString2 , 128 ) ;

/* ...и выведем результат на экран */

cout << "\n" << szString << endl ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

«Порядок аргументов функций str...( ) может показаться "обратным". Хотя смотря что считать правильным порядком. Например, strcat( targer , source ) дописывает source к концу target, что выглядит вполне логично.»

[Помни!]

Вот пример работы программы:

Введите строку #1: Шарик

Введите строку #2: собака

Шарик - собака

Press any key to continue...

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

«Кроме того, инструкция cin >> ничего не знает о длине строки. Она может прочесть тысячу символов и попытаться запихнуть их в массив, размер которого только 256 символов. Это опасно, кроме прочего, ещё и тем, что может послужить дырой, через которую хакеры смогут проникнуть в ваш компьютер...»

[Атас!]

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

cin.getline( string , lengthOfTheString ) ;

( Пока что не обращайте внимания на странную приставку cin.. )»

[Советы]

Функции strncpy( ) и strncat( ) в качестве одного из аргументов получают длину целевого буфера. Вызов strncpy( szString , szString1 , 128 ) означает "копировать в szString символы из szString1, пока не будет скопирован нулевой символ или пока не будет скопировано 128 символов". Это не означает, что всякий раз будет копироваться ровно 128 символов.

_________________

102 стр. Часть 2. Становимся функциональными программистами

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

[Атас!]

►Тип string...103

ANSI С++ предоставляет программисту тип string, облегчающий работу с символьными строками.

«Я использую термин строка для обозначения массива с завершающим нулевым символом; говоря о строках ANSI С++, я говорю о типе string. Тип string включает операции копирования, конкатенации, перевода строчных символов в прописные и т.п. функции. Они определены в заголовочном файле <string>.»

[Советы]

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

/* StringConcatenate — конкатенация двух строк с разделителем " - " */

#include <cstdio>

#include <cstdlib>

#include <iostream>

#include <string>

using namespace std ;

int main( int nNumberofArgs , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */

/* Считываем первую строку... */

string string1 ;

cout << "Введите строку #1:" ;

cin >> string1 ;

/* Считываем вторую строку... */

string string2 ;

cout << "Введите строку #2:" ;

cin >> string2 ;

/* Объединяем их в одном буфере */

string buffer ;

string divider = " - " ;

buffer = string1 + divider + string2 ;

/* ...и выводим результат */

cout << "\n" << buffer << endl ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

Здесь определены две переменные string1 и string2. Эти переменные не имеют определённой длины — они могут расти и уменьшаться в зависимости от того, сколько символов в них находится, вплоть до всей оперативной памяти.

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

_________________

103 стр. Глава 7. Хранение последовательностей в массивах

«Тип string не является встроенным типом С++, как int или float, т.е. операции с этим типом не встроены в синтаксис языка, а определены в заголовочном файле string. Детальнее класс string рассматривается в главе 27, "Шаблоны С++" ; здесь же я упомянул о нём только как о более простом средстве работы со строками.»

[Атас!]

_________________

104 стр. Часть 2. Становимся функциональными программистами

Глава 8. ПЕРВОЕ ЗНАКОМСТВО С УКАЗАТЕЛЯМИ В С++...105

ОГЛАВЛЕНИЕ

В этой главе...

►Размер переменной 105

►Что такое адрес 106

►Адресные операторы 106

►Использование указателей 108

►Передача указателей функциям 111

►Использование кучи 113

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

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

►Размер переменной...105

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

/* VariableSize — вывод информации о размерах переменных различных типов */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

int main( int nNumberofArgs , char* pszArgs[ ] )

{

bool b ;

char c ;

int n ;

long l ;

float f ;

double d ;

cout << "sizeof a bool  = " << sizeof b << endl ;

cout << "sizeof a char  = " << sizeof  c << endl ;

cout << "sizeof an int  = " << sizeof n << endl ;

_________________

105 стр. Глава 8. Первое знакомство с указателями в С++

cout << "sizeof a long  = " << sizeof l << endl ;

cout << "sizeof a float = " << sizeof f << endl ;

cout << "sizeof a double= " << sizeof d << endl ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

Оператор sizeof представляет собой специальную инструкцию С++, которая возвращает размер своего аргумента в байтах. Вот как выглядит вывод данной программы, скомпилированной Dev-C++:

sizeof a bool = 1

sizeof a char = 1

sizeof an int = 4

sizeof a long = 4

sizeof a float = 4

sizeof a double= 8

Press any key to continue...

«He удивляйтесь, если при использовании другого компилятора вы получите другие результаты. Например, может оказаться, что размер int меньше размера long. Стандарт С++ не оговаривает точные значения размера тех или иных типов — он говорит только о том, что размер типа int не превышает размера long, а размер double не может быть меньше размера float. Размеры, приведённые выше, типичны для 32-битовых процессоров ( типа Pentium ).»

[Советы]

►Что такое адрес...106

Очевидно, что каждая переменная С++ расположена где-то в памяти компьютера. Память разбита на байты, каждый из которых имеет свой адрес — 0, 1, 2 и т.д.

Переменная intRandy может находиться по адресу 0x100 , a floatReader — по адресу 0x180 ( адреса в памяти принято записывать в шестнадцатеричном виде ). Понятно, что эти переменные могут находиться где угодно, и только компьютер по долгу службы точно знает, где именно они располагаются — и то только в процессе выполнения программы.

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

►Адресные операторы...106

В табл. 8.1 приведены два оператора, связанные с указателями. Оператор & по сути говорит "скажи мне номер комнаты в отеле", а * — "кто в этой комнате живёт".

_________________

106 стр. Часть 2. Становимся функциональными программистами

Таблица 8.1. Адресные операторы

_________________

Оператор — Описание

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

& — ( унарный ) Адрес

* — ( унарный ) ( В выражении ) то, на что указывает указатель

* — ( унарный ) ( В объявлении ) указатель на

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

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

/* Layout — демонстрация расположения переменных в памяти компьютера */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

int main( int nNumberofArgs , char* pszArgs[ ] )

{

int end ;

int n ;

long l ;

float f ;

double d ;

/* Вывод в шестнадцатеричном формате */

cout.setf( ios::hex ) ;

cout.unsetf( ios::dec ) ;

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

cout << "--- = " << &end << " \n" ;

cout << "&n  = " << &n <<" \n" ;

cout << "&l  = " << &l <<" \n" ;

cout << "&f  = " << &f <<" \n" ;

cout << "&d  = " << &d <<" \n" ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

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

--- = 0x28ff34

&n  = 0x28ff30

&l  = 0x28ff2c

&f  = 0x28ff28

&d  = 0x28ff20

Press any key to continue...

_________________

107 стр. Глава 8. Первое знакомство с указателями в С++

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

[Советы]

Обратите внимание на то, что переменная n располагается ровно в 4 байтах от первой объявленной переменной end. Переменная  l располагается ещё на 4 байта ниже, а переменная типа double занимает 8 байт. Для каждой переменной выделяется память, необходимая для её типа.

«Стандарт С++ не требует от компилятора последовательного "беспросветного" размещения переменных в памяти. Dev-C++ может разместить переменные в памяти и по-другому.»

[Атас!]

►Использование указателей...108

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

Вот псевдокод, описывающий данную ситуацию:

mySon = &DadsRoom ; /* Теперь сын знает комнату отца */

room = *mySon ; /* "Номер  комнаты равен" */

Пример работы с операторами на С++ привёден в следующем листинге:

void fn( )

{

int intVar ;

int* pintVar ;

pintVar = &intVar ; /* Теперь pintVar указывает на intVar */

*pintVar =10 ; /* Сохраняет 10 в переменной типа int по адресу, находящемуся в pintVar */

}

Функция fn( ) начинается с объявления переменной intVar ; в следующей строке объявляется pintVar — указатель на переменную типа int.

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

При написании программ желательно придерживаться соглашений об именах, в соответствии с которыми первый символ в названии переменной указывает на её тип. Например, можно использовать n для int, d для double и т.д. С учётом этого соглашения имена указателей далее в книге будут начинаться с буквы р.

Унарный оператор & в выражении означает "взять адрес переменной". Таким образом, в первой строке приведённого кода находится команда сохранения адреса переменной intVar в переменной pintVar.

Представим себе, что функция fn( ) начинается с адреса 0x100 , переменная intVar расположена по адресу 0x102, а указатель pintVar — 0x106 ( такое расположение намного проще результатов работы программы Layout ; на самом деле вряд ли переменные будут храниться в памяти именно в таком порядке ).

_________________

108 стр. Часть 2. Становимся функциональными программистами

Первая команда программы сохраняет значение &intVar ( 0x102 ) в указателе pintVar. Вторая строка отвечает за присвоение значения 10 переменной, хранящейся по адресу, который содержится в указателе pintVar ( в нём находится число 0x102, т.е. адрес переменной intVar ).

Сравнение указателей и почтовых адресов...109

Указатели похожи на адреса домов. Ваш дом имеет уникальный адрес, и каждый байт в памяти компьютера тоже имеет уникальный адрес. Почтовый адрес содержит набор цифр и букв. Например, он может выглядеть так: 123 Main Street ( конечно же, это не мой адрес! Я не люблю нашествий поклонников, если только они не женского пола ).

Можно хранить диван в доме по адресу 123 Main Street, и точно так же можно хранить число в памяти по адресу 0x123456. Можно взять лист бумаги и написать на нём адрес — 123 Main Street. Теперь диван хранится в доме, который находится по адресу, написанному на листке бумаги. Так работают сотрудники службы доставки: они доставляют диваны по адресу, который указан в бланке заказа, независимо от того, какой именно адрес записан в бланке ( я ни в коем случае не смеюсь над работниками службы доставки — просто это самый удобный способ объяснить указатели ).

Использовав синтаксис С++, это можно записать так:

House myHouse ;

House* houseAddress ;

houseAddress = &myHouse ;

*houseAddress = couch ;

Эта запись обозначает следующее: myHouse является домом, a houseAddress — адресом дома. Надо записать адрес дома myHouse в указатель houseAddress и доставить диван по адресу, который находится в указателе houseAddress. Теперь используем вместо дома переменную типа int:

int myInt ;

int* intAddress ;

intAddress = &myInt ;

*intAddress = 10 ;

Аналогично предыдущей записи, это поясняется так: myInt — переменная типа int. Следует сохранить адрес myInt в указателе intAddress и записать 10 в переменную, которая находится по адресу, указанному в intAddress.

Использование разных типов указателей...109

Каждое выражение, как и переменная, имеет свой тип и значение. Тип выражения &intVar — указатель на переменную типа int, т.е. это выражение имеет тип int*. При сравнении его с объявлением указателя pintVar становится очевидно, что они одинаковы:

int* pintVar = &intVar ; /*  Обе части этого присвоения имеют тип *int  */

Аналогично pintVar имеет тип int* , a *pintVar — тип int:

*pintVar = 10 /* Обе части этого присвоения имеют тип int */

Тип переменной, на которую указывает pintVar, — int. Это эквивалентно тому, что если houseAddress является адресом дома, то, как ни странно, houseAddress указывает дом. Указатели на переменные других типов объявляются точно так же:

double doubleVar

double* pdoubleVar = &doubleVar

*pdoubleVar = 10.0

_________________

109 стр. Глава 8. Первое знакомство с указателями в С++

В компьютере класса Pentium размер указателя равен четырём байтам, независимо от того, на переменную какого типа он указывает[ 13 ].

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

int n1 ;

int* pintVar ;

pintVar = &n1 ;

*pintVar = 100.0 ;

Последняя строка требует, чтобы по адресу, выделенному под переменную размером в четыре байта, было записано значение, имеющее размер восемь байтов. На самом деле ничего страшного не произойдёт, поскольку в этом случае компилятор приведёт 100.0 к типу int перед тем, как выполнить присвоение.

Привести переменную одного типа к другому явным образом можно так:

int iVar ;

double dVar = 10.0 ;

iVar = ( int )dVar ;

Так же можно привести и указатель одного типа к другому:

int* piVar ;

double dVar = 10.0 ;

double* pdVar ;

piVar = ( int* )pdVar ;

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

/* LayoutError — демонстрирует результат неаккуратного обращения с указателями */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

int main( int intArgc , char* pszArgs[ ] )

{

/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */

setlocale ( LC_ALL , ".1251" ) ;

int upper = 0 ;

int n = 0 ;

int lower = 0 ;

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

cout << "&upper = 0x" << &upper<< "\n" ;

cout << "&n      = 0x" << &n  << "\n" ;

cout << "&lower  = 0x" << &lower << "\n" ;

/* Выводим значения объявленных переменных */

cout << "upper = " << upper << "\n" ;

cout << "n = " << n << "\n" ;

cout << "lower = " << lower << "\n" ;

/* Сохраняем значение типа double в памяти, выделенной для int */

cout << "\nСохранение double в int\n\n" ;

cout << "\nСохранение 13.0 по адресу &n\n\n" ;

double* pD = ( double* )&n ;

*pD = 13.0 ;

/* Показываем результаты */

cout << "upper = " << upper << "\n" ;

cout << "n = " << n << "\n" ;

cout << "lower = " << lower << "\n" ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system ( "PAUSE" ) ; return 0 ;

}

__________

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

_________________

110 стр. Часть 2. Становимся функциональными программистами

В первых трёх строках функции main( ) происходит объявление трёх переменных типа int. Допустим, что в памяти эти переменные находятся друг за другом.

Следующие три строки выводят значения этих переменных на экран. Не удивительно, что все три оказываются равными нулю. После этого происходит присвоение *pD = 13.0 ; , в результате которого число, имеющее тип double, записывается в переменную n, имеющую тип int. Затем все три переменные снова выводятся на экран.

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

upper = 0

n = 0

lower = 0

Сохранение double в int

upper = 1076494336

n = 0

lower = 0

Press any key to continue...

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

house* houseAddress = &"123 Main Street" ;

hotel* hotelAddress ;

hotelAddress = ( hotel* )houseAddress ;

*hotelAddress = TheRitz ;

Указатель houseAddress инициализирован как указатель на мой дом. Переменная hotelAddress содержит адрес отеля. После этого вместо адреса моего дома записывается адрес отеля. Затем отель "Ритц" устанавливается по адресу моего дома. Однако поскольку "Ритц" куда больше моего дома, не удивительно, что он уничтожит не только мой дом, но и дома моих соседей ( хоть что-то приятное в результате ошибки! ).

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

►Передача указателей функциям...111

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

_________________

111 стр. Глава 8. Первое знакомство с указателями в С++

Передача аргументов по значению...112

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

void fn( intArg )

{

int intArg = 10 ;

/* Здесь значение intArg равно 10 */

}

void parent( void )

{

int n1 = 0 ;

fn( n1 ) ;

/* Здесь n1 равно 0 */

}

Функция parent( ) инициализирует переменную n1 нулём. После этого значение n1 передаётся в качестве аргумента функции fn( ). В fn( ) переменной intArg присваивается значение 10 , тем самым в fn( ) осуществляется попытка изменить аргумент функции. Поскольку в качестве аргумента выступает переменная n1, можно ожидать, что после возврата в parent( ) эта переменная должна иметь значение 10. Тем не менее n1 остаётся равной 0.

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

«Некоторые программисты, стараясь не быть многословными, говорят что-то вроде "передаём переменную х функции fn( )". На самом деле это означает, что функции fn( ) передаётся значение выражения х.»

[Атас!]

Передача значений указателей...112

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

void fn( int* pintArg )

{

*pintArg = 10 ;

}

void parent( void )

{

int n = 0 ;

fn( &n ) ; /* Так передаётся адрес n */

/* теперь n равно 10 */

}

В этом случае вместо значения n функции fn( ) передаётся адрес этой переменной. Чем отличается передача значения переменной от передачи значения указателя на переменную, станет понятно, если рассмотреть присвоение, выполняющееся в функции fn( ).

Предположим, что n находится по адресу 0x102. В этом случае функции fn( ) передаётся аргумент, равный 0x102. Внутри fn( ) присвоение *pintArg = 10 выполняет запись целого значения 10 в переменную типа int, которая находится по адресу 0x102. Таким образом, нуль в переменной n заменяется на 10 , поскольку в данном случае 0x102 и есть адрес переменной n.

_________________

112 стр. Часть 2. Становимся функциональными программистами

Передача аргументов по ссылке...113

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

void fn( int& intArg )

{

intArg = 10 ;

}

void parent( void )

{

int n = 0 ;

fn ( n )

/* Теперь значение n равно 10 */

}

В этом примере функция fn( ) получает не значение переменной n, а ссылку на неё и, в свою очередь, записывает 10 в переменную типа int, на которую ссылается intArg.

►Использование кучи...113

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

«Visual С++ .NET позволяет программисту писать код, который работает в т.н. управляемом режиме ( managed mode ), когда выделение и освобождение памяти обрабатывает компилятор. Поскольку таким режимом отличается только Visual С++ .NET, в данной книге он не рассматривается.»

[Атас!]

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

double* fn( void ) ;

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

Область видимости...113

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

/* Эта переменная доступна для всех функций и существует на протяжении всего времени работы программы ( глобальная область видимости ) */

int intGlobal ;

/* Переменная intChild доступна только в функции child( ) и существует только во время выполнения функции child( ) или вызываемой ею ( область видимости функции ) */

_________________

113 стр. Глава 8. Первое знакомство с указателями в С++

void child( void )

{

int intChild ;

}

/* Переменная intParent имеет область видимости функции */

void parent( void )

{

int intParent = 0 ;

child( ) ;

int intLater = 0 ;

intParent = intLater ;

}

int main( int nArgs , char* pArgs[ ] )

{

parent( ) ;

}

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

Во второй строке parent( ) вызывается функция child( ). Эта функция также объявляет локальную переменную — intChild, областью видимости которой является функция child( ). При этом intParent функции child( ) недоступна ( и область видимости intParent не распространяется на функцию child( ) ), но сама переменная продолжает существовать.

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

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

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

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

Проблемы области видимости...114

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

double* child( void )

{

double dLocalVariable ;

return &dLocalVariable ;

}

void parent( void )

{

double* pdLocal ;

pdLocal = child( ) ;

*pdLocal = 1.0 ;

}

_________________

114 стр. Часть 2. Становимся функциональными программистами

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

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

[Атас!]

Использование блока памяти...115

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

Память в куче можно выделить, используя оператор new ; он пишется вместе с типом объекта, под который нужно выделить память. Приведённый ниже пример выделяет из кучи память для переменной типа double.

double* child( void )

{

double* pdLocalVariable = new double ;

return pdLocalVariable ;

}

Теперь, несмотря на то что переменная pdLocalVariable имеет область видимости в пределах функции child( ), память, на которую указывает эта переменная, не будет освобождена после выполнения функции. Выделение и освобождение памяти в куче осуществляется только явно. Освобождение памяти в куче выполняется с помощью команды delete.

void parent( void )

{

/* функция child( ) возвращает адрес переменной в куче */

double* pdMyDouble = child( ) ;

/* сохранение значения в созданной переменной */

*pdMyDouble = 1.1 ;

// ...

/* возврат памяти куче */

delete pdMyDouble ;

pdMyDouble = 0 ;

// ...

}

_________________

115 стр. Глава 8. Первое знакомство с указателями в С++

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

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

[Советы]

_________________

116 стр. Часть 2. Становимся функциональными программистами

Глава 9. ВТОРОЕ ЗНАКОМСТВО С УКАЗАТЕЛЯМИ...117

ОГЛАВЛЕНИЕ

В этой главе...

►Операции с указателями 117

►Объявление и использование массивов указателей 124

Язык С++ позволяет работать с указателями так, как если бы они были переменными простых типов. ( Концепция указателей изложена в главе 8, "Первое знакомство с указателями в С++". ) Однако операции над указателями требуют знания некоторых тонкостей; именно они и рассматриваются в этой главе.

►Операции с указателями...117

Некоторые из операций, описанных в главе 3, "Выполнение математических операций", могут быть применены и к указателям. В этом разделе рассматривается использование арифметических операций при работе с указателями и массивами ( с массивами вы познакомились в главе 7, "Хранение последовательностей в массивах" ). В табл. 9.1 приведены базовые операции над указателями.

Таблица 9.1. Три операции над указателями

_________________

Операция — Результат — Действие

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

pointer+offset — Указатель — Вычисляет адрес элемента, расположенного через offset элементов после pointer

pointer-offset — Указатель — Операция, противоположная сложению

pointer2-pointer1 — Смещение — Вычисляет количество элементов между pointer1 и pointer2

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

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

Модель памяти, построенная на примере домов ( так эффективно использованная в предыдущей главе ), поможет понять, как работают приведённые в таблице операции с указателями. Представьте себе квартал, в котором все дома пронумерованы по порядку. Дом, следующий за домом 123 Main Street, будет иметь адрес 124 Main Street ( или 122 Main Street, если вы идёте в противоположную сторону, поскольку вы левша или живёте в Англии ).

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

123 Main Street + 4 = 127 Main Street

_________________

117 стр. Глава 9. Второе знакомство с указателями

И наоборот, если поинтересоваться, сколько домов находится между домом 123 и 127, ответом будет четыре:

127 Main Street - 123 Main Street = 4

Понятно, что любой дом находится относительно самого себя на расстоянии нуль домов:

123 Main Street - 123 Main Street = 0

Продолжая рассуждения, становится понятно, что складывать дома 123 и 127 не имеет никакого смысла. Соответственно, суммирование двух указателей является в С++ некорректной операцией. Вы также не можете умножать или делить адреса, возводить их в квадрат или извлекать квадратный корень — словом, надеюсь, вы поняли, что я хотел сказать.

Повторное знакомство с массивами в свете указателей...118

Обратимся к странному и мистическому миру массивов. Ещё раз воспользуемся в качестве примера домами моих соседей. Массив тоже очень похож на городской квартал. Каждый элемент массива выступает в качестве дома в этом квартале. Дома — элементы массива — отсчитываются по порядку от начала квартала. Дом на углу улицы отстоит на 0 домов от угла, следующий дом отстоит на 1 дом от угла и т.д. Пользуясь терминологией массивов, можно сказать, что cityBlock[ 0 ] представляет собой дом по адресу 123 Main Street, cityBlock[ 1 ] — дом по адресу 124 Main Street и т.д.

Теперь представим себе массив из 32-х однобайтовых значений, имеющий имя charArray. Если первый элемент массива находится по адресу 0x110 , тогда массив будет продолжаться вплоть до адреса 0x12f. Таким образом, элемент массива charArray[ 0 ] находится по адресу 0x110 , charArray[ 1 ] — по адресу 0x111 , charArray[ 2 ] — по адресу 0x112 и т.д.

Можно создать указатель ptr на нулевой элемент массива. После выполнения строки ptr = &charArray[ 0 ] ; указатель ptr будет содержать адрес 0x110. Можно прибавить к этому адресу целочисленное смещение и перейти к необходимому элементу массива. Операции над массивами с использованием указателей приведены в табл. 9.2. Эта таблица демонстрирует, каким образом добавление смещения n вызывает переход к следующему элементу массива charArray.

Таблица 9.2. Добавление смещения

_________________

Смещение — Результат — Соответствует

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

+0 — 0x110 — charArray[ 0 ]

+1 — 0x111 — charArray[ 1 ]

+2 — 0x112 — charArray[ 2]

... — ... — ...

+n — 0х110+n — charArray[ n ]

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

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

Таким образом, если char* ptr — &charArray[ 0 ] ;, то *( ptr + n ) соответствует элементу charArray [ n ].

«Поскольку * имеет более высокий приоритет, чем сложение, операция *ptr + n привела бы к сложению n со значением, на которое указывает ptr. Чтобы выполнить сначала сложение и лишь затем переход к переменной по указателю, следует использовать скобки. Выражение *( ptr + n ) возвращает элемент, который находится по адресу ptr плюс n элементов.»

[Атас!]

_________________

118 стр. Часть 2. Становимся функциональными программистами

В действительности соответствие между двумя формами выражений настолько строго, что С++ рассматривает элемент массива array[ n ] как *( ptr + n ), где ptr указывает на первый элемент array. С++ интерпретирует array[ n ] как *( &аrray [ 0 ] +n ). Таким образом, если дано char charArray[ 20 ], то charArray определяется как &charArray[ 0 ].

Имя массива, записанное без индекса элемента, интерпретируется как адрес нулевого элемента массива ( или просто адрес массива ). Таким образом, можно упростить приведённую выше запись, поскольку array[ n ] С++ интерпретирует как *( array + n ).

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

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

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

/* displayArray — отображает элементы массива, имеющего длину nSize */

void displayArray( int intArray[ ] , int nSize )

{

cout << "Значения элементов массива равны:\n" ;

for ( int n = 0 ; n < nSize ; n++ )

{

cout << n << ": " << intArray[ n ] << "\n" ;

}

cout << "\n" ;

}

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

/* displayArray — отображает элементы массива, имеющего длину nSize */

void displayArray( int intArray[ ] , int nSize )

{

cout << "Значения элементов массива равны:\n" ;

int* pArray = intArray ;

for ( int n = 0 ; n < nSize ; n++ , pArray++ )

{

cout << n << ": " << *pArray << "\n" ;

}

cout << "\n" ;

}

Этот вариант функции displayArray начинается с создания указателя на первый элемент массива intArray.

«Буква р в начале имени переменной означает, что эта переменная является указателем, однако это только соглашение, а не стандарт языка С++.»

[Помни!]

_________________

119 стр. Глава 9. Второе знакомство с указателями

После этого функция считывает все элементы массива по порядку. При каждом выполнении оператора for происходит вывод текущего элемента из массива intArray. Этот элемент находится по адресу рArray, который увеличивается на единицу при каждом выполнении цикла.

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

int main( int nNumberOfArgs , char* pszArgs[ ] )

{

int array[ ] = { 4 , 3 , 2 , 1 } ;

displayArray( array , 4 ) ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

Результат работы этой программы имеет следующий вид:

Значения элементов массива равны:

0: 4

1: 3

2: 2

3: 1

Press any key to continue...

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

Использование указателей для работы со строками...120

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

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

/* DisplayString — вывод символьного массива с использованием указателей и индексов массива */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

int main( int nNumberofArgs , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */

/* Объявляем строку */

char* szString = "Randy" ;

_________________

120 стр. Часть 2. Становимся функциональными программистами

cout << "Массив ' " << szString << " ' " << endl ;

/* Выводим szString как массив */

cout << "Выводим строку как массив: " ;

for ( int i = 0 ; i < 5 ; i++ )

{

cout << szString[ i ] ;

}

cout << endl ;

/* Воспользуемся арифметикой указателей */

cout << "Выводим строку с помощью указателя: " ;

char* pszString = szString ;

while ( *pszString )

{

cout << *pszString ;

pszString++ ;

}

cout << endl ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

Программа сначала проходит по массиву szString с использованием индекса массива. Цикл for прекращает работу, когда индекс достигает значения 5, равного длине строки.

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

«Целое значение 0 в С++ рассматривается как false, прочие целочисленные значения — как true.»

[Помни!]

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

«Разыменование и инкремент могут быть объединены ( обычно так и делается ) в единое выражение следующим образом:

cout << *pszString++ ;

»

[Помни!]

Вывод данной программы выглядит так:

Массив ' Randy '

Выводим строку как массив: Randy

Randy Выводим строку с помощью указателя: Randy

Press any key to continue...

_________________

121 стр. Глава 9. Второе знакомство с указателями

Почему при работе со строками пользуются указателями...122

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

Ответ следует искать отчасти в человеческой природе, отчасти в истории развития С++. Компилятор языка С, прародителя С++, в те времена, когда язык появился на свет, был довольно примитивен. Тогда компиляторы не были столь сложными, как сейчас, и не могли так хорошо оптимизировать код. Код while ( *pszTarget++ = *pszSource++ ){ } может показаться читателю сложным, однако после компиляции с помощью даже самого древнего компилятора он будет состоять буквально из нескольких машинных инструкций.

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

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

«Не надейтесь, что, написав сложное и запутанное выражение на С++, вы сэкономите несколько машинных команд. В С++ нет прямой связи между количеством команд в исходном и конечном коде.»

[Советы]

Операции с указателями других типов...122

Нетрудно сообразить, что szTarget+n указывает на элемент szTarget[ n ], если szTarget является массивом однобайтовых значений. Если szTarget начинается по адресу 0x100 , то шестой элемент массива будет находиться по адресу 0x105.

Однако положение элемента в массиве становится не столь очевидным, если массив состоит из элементов типа int, которые занимают по четыре байта каждый. Если первый элемент такого массива находится по адресу 0x100 , то шестой будет находиться по адресу 0x114( 0x100 + ( 5 * 4 ) = 0x114 ).

Но, к счастью для нас, выражение вида array + n будет всегда указывать на элемент array[ n ], независимо от размера элемента, поскольку в таком выражении С++ самостоятельно учитывает длину элемента.

И вновь обратимся за аналогией к моему дому. Третий дом от 123 Main Street будет иметь адрес 126 Main Street, независимо от размеров стоящих на Main Street домов.

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

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

void arrayPointer( )

{

/* Выделение памяти для 128 символов */

char charArray[ 128 ] ;

/* Выделение памяти для  указателя, но не для объекта, на который он указывает */

char* pArray ;

}

_________________

122 стр. Часть 2. Становимся функциональными программистами

В этом примере для charArray выделяется 128 байт, а для pArray — четыре, ровно столько, сколько необходимо для хранения указателя. Приведённая ниже функция работать не будет.

void arrayVsPointer( )

{

/* Этот фрагмент будет работать нормально */

char charArray[ 128 ] ;

charArray[ 10 ] = '0' ;

*( charArray + 10 ) = '0' ;

/* Этот фрагмент не будет работать так, как надо */

char* pArray ;

pArray[ 10 ] = '0' ;

*( pArray + 10 ) = '0' ;

}

Выражения charArray[ 10 ] и *( charArray + 10 ) с позиции компилятора эквивалентны и вполне законны. Те же выражения с использованием pArray являются бессмысленными. Несмотря на то что для С++ они являются законными, pArray не инициализирован как указатель на массив, а значит, память была выделена только для указателя. Таким образом, рАггау[ 10 ] и *( рАггау + 10 ) указывают на неизвестные и непредсказуемые значения.

«Неправильно инициализированные указатели обычно вызывают ошибку нарушения сегмента ( segment violation ). Эту ошибку вы иногда встречаете в повседневной работе со своими любимыми приложениями в своей любимой ( а может, и не очень ) операционной системе.»

[Советы]

Второе отличие между указателями и индексами массива состоит в том, что charArray — константа, тогда как pArray — нет. Приведённый ниже цикл for, который должен инициализировать значения элементов массива, тоже не будет работать.

void arrayVsPointer( )

{

char charArray[ 10 ] ;

for ( int i = 0 ; i < 10 ; i++ )

{

*charArray = '\0' ; /* Эта строка имеет смысл... */

charArray++ ; /* ... а эта нет */

}

}

Выражение charArray++ имеет не больше смысла, чем 10++. Правильно следует написать так:

void arrayVsPointer( )

{

char charArray[ 10 ] ;

char* pArray = charArray ;

for ( int i = 0 ; i < 10 ; i++ )

{

*pArray = '\0' ; /* Этот вариант будет  работать так, как надо */

pArray++ ;

}

}

_________________

123 стр. Глава 9. Второе знакомство с указателями

►Объявление и использование массивов указателей...124

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

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

int* pInts[ 10 ] ;

Таким образом, элемент pInts[ 0 ] является указателем на переменную типа int. Следовательно, приведённый ниже код корректен:

void fn( )

{

int n1 ;

int* pInts[ 3 ] ;

pInts[ 0 ] = &n1 ;

*pInts[ 0 ] = 1 ;

}

Как и этот:

void fn( )

{

int n1 , n2 , n3 ;

int* pInts[ 3 ] = { &n1 , &n2 , &n3 } ;

for ( int i = 0 ; i < 3 ; i++ )

{

*pInts[ i ] = 0 ;

}

}

И даже этот:

void fn( )

{

int n1 , n2 , n3 ;

int* pInts[ 3 ] = { ( new int ) ,

( new int ) ,

( new int ) } ;

for ( int i = 0 ; i < 3 ; i++ )

{

*pInts[ i ] = 0 ;

}

}

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

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

Использование массивов строк...124

Допустим, мне понадобилась функция, возвращающая название месяца по его номеру. Например, если этой функции передать число 1, она вернёт название первого месяца — "Январь". Номер месяца будет считаться неправильным, если он окажется меньше 1 или больше 12.

_________________

124 стр. Часть 2. Становимся функциональными программистами

Эту функцию можно написать следующим образом:

/* int2month( ) — возвращает название месяца */

char* int2month( int nMonth )

{

char* pszReturnValue ;

switch( nMonth )

{

case 1 : pszReturnValue = "Январь" ;

break ;

case 2 : pszReturnValue = "Февраль" ;

break ;

case 3 : pszReturnValue = "Март" ;

break ;

/* и так далее... */

default : pszReturnValue = "Неверный номер месяца"

}

return pszReturnValue ;

}

«Оператор switch( ) действует так же, как совокупность операторов if.»

[Помни!]

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

/* int2month( ) — возвращает название месяца */

char* int2month( int nMonth )

{

/* проверка правильности номера месяца */

if ( nMonth < 1 || nMonth > 12 )

{

return "invalid" ;

}

/* nMonth имеет корректное значение */

/* Вернём имя месяца */

char* pszMonths[ ] = { "Ошибка" ,

"Январь" ,

"Февраль" ,

"Март" ,

"Апрель" ,

"Май" ,

"Июнь" ,

"Июль" ,

"Август" ,

"Сентябрь" ,

"Октябрь" ,

"Ноябрь" ,

"Декабрь" } ;

return pszMonths[ nMonth ] ;

}

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

_________________

125 стр. Глава 9. Второе знакомство с указателями

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

[Советы]

Доступ к аргументам main( )...126

Второй аргумент функции main( ) — массив указателей на строки. Эти строки содержат аргументы, передаваемые программе при вызове. Допустим, я ввёл следующее в командной строке MS DOS:

MyProgram file.txt /w

MS DOS запустит программу, которая находится в файле MyProgram.ехе, и передаст ей как аргументы file.txt и /w. Аргументы, начинающиеся с косой черты ( / ) или дефиса ( - ), обрабатываются операционной системой, как и любые другие: они передаются программе, чтобы та разбиралась с ними сама. Аргументы, которые начинаются с <, >, >> или || ( а иногда и некоторые другие ), представляют особый интерес для операционных систем и программе не передаются.

Аргументы программы являются одновременно аргументами функции main( ). Переменная pszArgs, передаваемая main( ), содержит массив указателей на аргументы программы, a nArg — их количество.

Ниже приведён пример считывания аргументов из командной строки.

/* PrintArgs — выводит аргументы программы в стандартный вывод операционной системы */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

int main( int nArg , char* pszArgs[ ] )

{

setlocale (LC_ALL,".1251"); /* печать русских текстов */

/* Выводим заголовок */

cout << "Аргументами программы " << pszArgs[ 0 ]

<< " являются\n" ;

/* Выводим аргументы программы */

for ( int i = 1 ; i < nArg ; i++ )

{

cout << i << ": " << pszArgs[ i ] << "\n" ;

}

// Вот и всё

cout << "Вот и всё \n" ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

_________________

126 стр. Часть 2. Становимся функциональными программистами

Как всегда, функция main( ) получает два аргумента. Первый — переменная типа int, которую я назвал nArg. Эта переменная содержит количество передаваемых программе аргументов. Вторая переменная содержит массив указателей типа char*; её я назвал pszArgs. Каждый из этих указателей ссылается на один из аргументов программы.

Аргументы в DOS...127

Если запустить программу PrintArgs с аргументами

PrintArgs arg1 arg2 arg3 /w

из командной строки MS DOS, nArg будет равняться 5 ( по количеству аргументов ). Первый аргумент — имя самой программы. Таким образом, pszArgs [ 0 ] будет указывать на имя запускаемого файла, а остальные четыре указателя — на оставшиеся четыре аргумента ( в данном случае это "arg1", "arg2", "arg3" и "/w" ). Поскольку MS DOS никак не выделяет символ /, последний аргумент будет представлять собой строку "/w".

Аргументы в Dev-C++...127

Для того, чтобы передать аргументы программе при запуске её в среде Dev-C++, можно воспользоваться командой меню Debugs => Parameters ( Отладкам => Параметры ). Введите нужные вам параметры и запустите программу на выполнение при помощи меню Ехесute => Run ( Выполнить => Выполнить ) либо клавиш <Ctrl+F10>.

Аргументы в Windows...127

Windows использует аргументы как средство коммуникации с программой. Проведите следующий эксперимент. Соберите описанную программу и найдите её с помощью Windows Explorer. Например, пусть она имеет имя X:\Cpp_Program\Chap09\PrintArgs.exe. Возьмите произвольный файл и перетащите его на имя файла программы — после этого запустится программа PrintArgs, и вы увидите имя перемещённого файла. Попробуйте перетащить несколько файлов одновременно ( выделив их при нажатой клавише <Ctrl> или выделив группу при помощи клавиши <Shift>). Программа выведет имена всех перемещённых вами файлов.

Вот как выглядит вывод программы, если перетащить на неё файлы из папки Dev-C++.

Аргументами программы E:\Tmp\PrintArgs.exe являются

1: C:\Dev-Cpp\devcpp.exe

2: C:\Dev-Cpp\copying.txt

3: C:\Dev-Cpp\NEWS.txt

4: C:\Dev-Cpp\Packman.exe

5: C:\Dev-Cpp\uninstall.exe

6: C:\Dev-Cpp\vRoach.exe

7: C:\Dev-Cpp\vUpdate.exe

Вот и всё

Press any key to continue...

Обратите внимание, что каждое имя файла представлено как отдельный аргумент; кроме того, как видите, Windows передаёт в качестве параметра полное имя файла.

_________________

127 стр. Глава 9. Второе знакомство с указателями

Глава 10. ОТЛАДКА ПРОГРАММ НА С++...128

ОГЛАВЛЕНИЕ

В этой главе...

►Определение типа ошибки 128

►Использование отладочной печати 128

►Использование отладчика  134

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

Чтобы избавиться от ошибок, можно пойти двумя путями. Первый — стереть программу и написать её заново, а второй — найти и исправить ошибки. Освоение первого пути я оставляю читателю, а в этой главе расскажу о том, как выследить и исправить ошибку в программе.

►Определение типа ошибки...127

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

Ошибки, которые компилятор не может найти, проявляются при запуске программы и называются ошибками времени исполнения ( run-time error ). Их найти намного труднее, поскольку, кроме сообщения об ошибке, нет и намёка на то, какая именно ошибка возникла и где ( сообщения, которые генерируются при возникновении ошибок выполнения, вполне достойны "звания" ошибочных ).

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

►Использование отладочной печати...128

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

_________________

128 стр. Часть 2. Становимся функциональными программистами

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

«Данная программа имеется на прилагаемом компакт-диске под именем ErrorProgram1.срр.»

[Диск]

/* ErrorProgram — эта программа усредняла бы ряд чисел, если бы не содержала как минимум одну фатальную ошибку */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

int main( int nNumberOfArgs , char* pszArgs[ ] )

{

/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */

setlocale (LC_ALL,".1251");

cout << "Эта программа содержит ошибки!\n" ;

int nSum ;

int nNums ;

/* Суммируем ряд чисел, пока пользователь не введёт отрицательное число, после чего выводим среднее */

nNums = 0 ;

while ( true )

{

/* Ввод очередного числа */

int nValue ;

cout << "Введите следующее число:" ;

cin >> nValue ;

cout << endl ;

/* Если это число отрицательное... */

if ( nValue < 0 )

{

/* ...то вывести среднее значение */

cout << "Среднее равно: "

<< nSum / nNums

<< endl ;

break ;

}

/* Введеённое число не отрицательно — добавляем его к накапливаемой сумме */

nSum += nValue ;

}

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

_________________

129 стр. Глава 10. Отладка программ на С++

После ввода этой программы создайте выполнимый файл ( клавиша <F9> ). Запустите эту программу и введите числа 1, 2 и 3, а затем -1. Вы ожидаете увидеть, что их среднее равно двум? Вместо этого очевидного результата будет выдано довольно непривлекательное сообщение об ошибке, показанное на рис. 10.1.

«Рис. 10.1 может отличаться в зависимости от используемых операционной системы и компилятора.»

[Помни!]

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

Выявление "жучка" № 1...130

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

cout << "Среднее равно: "

<< nSum / nNums

<< endl ;

«Кстати говоря, хотя это и единственное деление, использованное нами в программе, это ещё не означает, что ошибка именно в нём. Компилятор может сгенерировать команду деления в результате обработки некоторой иной инструкции, написанной программистом. Кроме того, делений хватает и в стандартной библиотеке С++.»

[Советы]

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

while ( true )

{

cout << "nNums = " << nNums << endl ;

/* Остальная часть программы остаётся неизменной */

_________________

130 стр. Часть 2. Становимся функциональными программистами

Такое дополнение нашей программы приводит к следующему выводу на экран:

Эта программа содержит ошибки!

nNums = 0

Введите следующее число: 1

nNums = 0

Введите следующее число: 2

nNums = 0

Введите следующее число: 3

nNums = 0

Введите следующее число:

Как видите, nNums инициализировано нулевым значением, но оно не увеличивается в процессе ввода новых чисел. Это неверно, и именно в этом состоит ошибка в программе. Очевидно, что количество введённых чисел nNums должно увеличиваться, чего можно легко достичь, заменив цикл while циклом for:

for ( int nNums = 0 ; ; nNums++ )

Выявление "жучка" № 2...131

Теперь, когда найдена и исправлена ошибка № 1, можно запустить программу, введя числа, заставившие её в прошлый раз аварийно завершиться. На этот раз сообщение об ошибке не появится и программа вернёт нулевой код выхода, но будет работать не так, как ожидалось. Вместо так горячо ожидаемой двойки будет выведено какое-то нелепое число.

Эта программа содержит ошибки:

Введите следующее число: 1

Введите следующее число: 2

Введите следующее число: 3

Введите следующее число: -1

Среднее равно: 229523

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

Для этого воспользуемся методом отладочной печати. Чтобы узнать значения nValue, nSum и nNums, перепишите тело цикла for так, как показано в следующем листинге ( версия программы имеется на прилагаемом компакт-диске в файле с именем ErrorProgram2.срр ).

/* ErrorProgram — эта программа усредняла бы ряд чисел, если бы не содержала как минимум одну фатальную ошибку */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

int main ( int nNumberofArgs , char* pszArgs[ ] )

{

/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */

setlocale (LC_ALL,".1251");

cout << " Эта программа содержит ошибки!"

<< endl ;

/* Суммируем ряд чисел, пока пользователь не введёт отрицательное число, после чего выводим среднее */

int nSum ;

for ( int nNums = 0 ; ; nNums++ )

_________________

131 стр. Глава 10. Отладка программ на С++

{

/* Ввод следующего числа */

int nValue ;

cout << "Введите следующее число:" ;

cin >> nValue ;

cout << endl ;

/* Если введённое число отрицательно... */

if ( nValue < 0 )

{

/* ...выводим результат усреднения */

cout << "\nСреднее равно: "

<< nSum / nNums

<< "\n" ;

break ;

}

/* Вывод отладочной информации */

cout << "nSum = " << nSum << "\n" ;

cout << "nNums= " << nNums << "\n" ;

cout << "nValue= " << nValue << "\n" ;

cout << endl ;

/* Введённое число не отрицательно, суммируем его */

nSum += nValue ;

}

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

Обратите внимание на то, что информация о состоянии отслеживаемых переменных nValue, nSum и nNums выводится в каждом цикле.

Ответ программы на ввод уже привычных 1, 2, 3 и -1 приведён ниже. При первом же проходе nSum принимает какое-то несуразное значение, хотя оно должно равняться нулю ( поскольку к этой переменной пока что ничего не прибавлялось ).

Эта программа содержит ошибки!

Введите следующее число:1

nSum = -858993460

nNums = 0

nValue= 1

Введите следующее число:2

nSum = -858993459

nNums= 1

nValue= 2

Введите следующее число:3

nSum = -858993457

nNums = 2

nValue= 3

Введите следующее число:

_________________

132 стр. Часть 2. Становимся функциональными программистами

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

int nSum = 0 ;

Примечание. Пока переменная не проинициализирована, её значение непредсказуемо.

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

[Диск]

/* ErrorProgram — эта программа усредняет ряд чисел и не содержит ошибок */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

int main( int nNumberofArgs , char* pszArgs[ ] )

{

/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */

setlocale (LC_ALL,".1251");

/* Суммируем ряд чисел, пока пользователь не введёт отрицательное число, после чего выводим среднее */

int nSum = 0 ;

for ( int nNums = 0 ; ; nNums++ )

{

/* Ввод следующего числа: */

int nValue ;

cout << "Введите следующее число:" ;

cin >> nValue ;

cout << endl ;

/* Если введённое число отрицательно... */

if ( nValue < 0 )

{

/* ...выводим усреднённое значение */

cout << "\nСреднее равно: "

<< nSum / nNums

<< "\n" ;

break ;

}

/* Введённое число не отрицательно, суммируем его */

nSum += nValue ;

}

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

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

Введите следующее число: 1

Введите следующее число: 2

Введите следующее число: 3

Введите следующее число: -1

Среднее равно: 2

Press any key to continue...

_________________

133 стр. Глава 10. Отладка программ на С++

►Использование отладчика...134

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

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

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

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

Что такое отладчик...134

Отладчик — это утилита, встроенная, например, в Dev-C++ или Microsoft Visual Studio .NET ( в этих приложениях программы отладчиков отличаются, однако работают они по одному принципу ).

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

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

Работа с отладчиком...134

В отличие от стандартизированного языка С++, набор команд, поддерживаемый отладчиком, варьируется от производителя к производителю. К счастью, большинство отладчиков поддерживают некоторый базовый набор команд. Необходимые нам команды есть как в Dev-С++, так и в Microsoft Visual С++ .NET; в них также имеется возможность вызова этих команд с помощью меню и функциональных клавиш. В табл. 10.1 приведён список основных команд и клавиш их вызова.

Таблица 10.1. Команды отладчиков Microsoft Visual С++ .NET и Dev-C++

_________________

Команда — Visual С++ — GNU С++ ( rhide )

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

Отладка — <F5> — <F8>

Шаг внутрь ( Step In ) — <F11> — <Shift+F7>

Следующий шаг ( Step Over ) — <F10> — <F7>

Продолжить выполнения — <F5> — <Ctrl+F7>

Просмотр переменной ( View Variable ) — Только в меню — Только в меню

Установка точки останова ( Set Breakpoint )* — <Ctrl+B> — <Ctrl+F5>

Добавить в наблюдаемые ( Add watch ) — Только в меню — <F4>

Перезагрузка программы ( Program Reset ) — <Shift+F5> — <Ctrl+Alt+F2>

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

__________

*Щелчок слева от строки исходного текста С++ в окне редактора представляет собой альтернативный путь установки точек останова.

_________________

134 стр. Часть 2. Становимся функциональными программистами

Запуск тестовой программы...135

«Лучший способ исправить ошибки в программе — пройти её пошагово. Приведённая ниже программа содержит несколько ошибок, которые надо найти и исправить. Эта программа имеется на прилагаемом компакт-диске в файле Concatenate1.срр.»

[Диск]

/* Concatenate - конкатенация двух строк */

/*                  со вставкой " - " между ними. В этой версии имеются ошибки. */

#include <cstdio>

#include <cstdlib>

#include <iostream>

#include <string.h>

using namespace std ;

void stringEmUp( char* szTarget ,

char* szSource1 ,

char* szSource2 ,

int nLength ) ;

int main( int nNumberofArgs , char* pszArgs[ ] )

{

/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */

setlocale (LC_ALL,".1251");

cout << "Конкатенация двух строк со вставкой \" - \"\n"

<< "( В этой версии имеются ошибки. )" << endl ;

char szStrBuffer[ 256 ] ;

/* Создание двух строк одинаковой длины... */

char szString1[ 16 ] ;

strncpy( szString1 , "This is a string" , 16 ) ;

char szString2[ 16 ] ;

strncpy( szString2 , "THIS IS A STRING" , 16 ) ;

/* ...и объединение их в одну */

stringEmUp( szStrBuffer ,

szString1 ,

szString2 ,

16 ) ;

// Вывод результата

cout << "<" << szStrBuffer << ">" << endl ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

void stringEmUp(char* szTarget,

char* szSource1,

char* szSource2,

int nLength)

{

strcpy( szTarget , szSource1 ) ;

strcat( szTarget , " - " ) ;

strcat( szTarget , szSource2 ) ;

}

_________________

135 стр. Глава 10. Отладка программ на С++

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

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

Первое, что стоит сделать при поиске ошибки с помощью отладчика, — это выполнить программу в отладочном режиме. Попытка выполнить эту программу в отладочном режиме в Dev-C++ ( с помощью клавиши <F8> ) приводит к появлению диалогового окна с сообщением об ошибке "Ваша программа вызвала нарушение доступа". Этой информации слишком мало, чтобы разобраться, в чём проблема.

«Подобное сообщение об ошибке обычно говорит о некорректной работе с указателями того или иного типа.»

[Советы]

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

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

Установим точку останова на первой выполнимой инструкции, щёлкнув слева от строки вывода в cout или воспользовавшись клавишами <Ctrl+F5>, как сказано в табл. 10.1. При этом вы увидите появившийся маленький красный кружок, говорящий об установленной точке останова ( рис. 10.2 ).

Теперь продолжим выполнение программы под отладчиком, либо выбирая команду меню Debug => Debug ( Отладка => Отладка ), либо щелчком на соответствующей пиктограмме в панели отладки, либо при помощи клавиши <F8>. Выполнение программы немедленно прекращается на первой же строке, а подсветка строки из красной делается синей, указывая, что выполнение программы заморожено на данной строке.

Теперь вы можете выбрать в меню команду Debug => Next Step ( Отладка => Следующий шаг ) либо нажать клавишу <F7> для выполнения одной строки программы.

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

_________________

136 стр. Часть 2. Становимся функциональными программистами

Рис. 10.2. Точку останова легко опознать по маленькому красному кружку

Рис. 10.3. В любой момент вы можете переключиться на окно выполняемой программы

Выполнение двух последующих инструкций приводит нас к вызову функции StringEmUp( ).

Если опять выбрать команду Debug  =>  Next Step ( Отладка  =>  Следующий шаг ), программа аварийно завершится. Теперь мы знаем, что проблема кроется в этой функции.

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

[Советы]

_________________

137 стр. Глава 10. Отладка программ на С++

Команда Debugs => Next Step ( Отладка => Следующий шаг ) рассматривает вызов функции как единую команду. Однако на самом деле функция состоит из ряда отдельных инструкций С++, и для отладки нам надо пройти их пошагово. Такая функциональность обеспечивается командой Debug => Step Into ( Отладка  =>  Шаг внутрь ).

Перегрузите программу при помощи пункта меню Debug => Program Reset ( Отладка => Остановить выполнение ) либо соответствующей пиктограммы на панели отладки или клавиш <Ctrl+Alt+F2>. Теперь, чтобы сэкономить время, отключите установленную точку останова, щёлкнув на красном кружке, и установите новую на вызове функции stringEmUp( ) , как показано на рис. 10.4.

Рис. 10.4. Установка точки останова на вызове функции stringEmUp( )

«Вы можете установить в программе столько точек останова, сколько вам требуется.»

[Советы]

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

Войдите в функцию, воспользовавшись командой Debug => Step into ( Отладка => Шаг внутрь ), как показано на рис. 10.5.

Допустим, вы обнаружили, что ваша программа время от времени работает некорректно. Чтобы лучше понять, почему это происходит, желательно знать, какие аргументы передаются в рассматриваемую функцию. Для этого нужна функция наблюдения за переменными, предоставляемая отладчиком, которая позволяет ознакомиться с содержимым всех переменных при каждом останове выполнения. Проще всего установить наблюдение за переменной, выбрав её в окне редактора и нажав клавишу <F4>. На рис. 10.6 показано, как выглядят четыре аргумента рассматриваемой функции.

_________________

138 стр. Часть 2. Становимся функциональными программистами

Рис. 10.5. Команда Debug => Step Into ( Отладка => Шаг внутрь ) позволяет выполнить вызов функции пошагово

Рис. 10.6. Отладчик позволяет следить за значениями переменных

_________________

139 стр. Глава 10. Отладка программ на С++

Числа возле имён переменных в окне отладки — адреса, которые в данном случае малоинформативны. Строка szTarget пока что пуста, что вполне закономерно, так как мы ещё ничего в неё не скопировали. Значение строки szString1 также выглядит вполне корректно, но вот значение szString2 содержит сразу две строки — и "This is a string", и "THIS IS A STRING", чего вроде бы быть не должно.

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

«Длина строки всегда включает завершающий нулевой символ.»

[Помни!]

Давайте изменим программу, исправив ошибку. Пусть теперь С++ сам рассчитывает размер строк. Получившаяся в результате программа Concatenate2.срр, приведённая ниже, работает вполне корректно.

/* Concatenate — конкатенация двух строк со вставкой " - " между ними. */

#include <cstdio>

#include <cstdlib>

#include <iostream>

#include <string.h>

using namespace std ;

void StringEmUp( char* szTarget ,

char* szSource1 ,

char* szSource2 ) ;

int main( int nNumberofArgs , char* pszArgs[ ] )

{

/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */

setlocale (LC_ALL,".1251");

cout << "Конкатенация двух строк со вставкой \" - \"\n"

<< "( В этой версии нет ошибки. )" << endl ;

char szStrBuffer[ 256 ] ;

/* Определение двух строк... */

char szString1[ ] = "This is a string" ;

char szString2[ ] = "THIS IS A STRING" ;

/* ...и объединение их в одну */

StringEmUp( szStrBuffer ,

szString1 ,

szString2 ) ;

/* Вывод результата */

cout << "<" << szStrBuffer << ">" << endl ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

_________________

140 стр. Часть 2. Становимся функциональными программистами

void StringEmUp( char* szTarget ,

char* szSource1 ,

char* szSource2 )

{

strcpy( szTarget , szSource1 ) ;

strcat( szTarget , " - " ) ;

strcat( szTarget , szSource2 ) ;

}

Вот вывод этой программы — именно такой, какой мы и ожидали:

Конкатенация двух строк со вставкой " - "

( В этой версии нет ошибки. )

<This is a string - THIS IS A STRING>

Press any key to continue...

! ! ! ! ! ! ! ! ! ! ! ! ! !

Поздравляю! Вам удалось отладить программу.

! ! ! ! ! ! ! ! ! ! ! ! ! !

_________________

141 стр. Глава 10. Отладка программ на С++

Часть 3. ВВЕДЕНИЕ В КЛАССЫ...143

ОГЛАВЛЕНИЕ

СОДЕРЖАНИЕ

Глава 11. ЗНАКОМСТВО С ОБЪЕКТНО-ОРИЕНТИРОВАННЫМ ПРОГРАММИРОВАНИЕМ...145

Глава 12. КЛАССЫ В С++...149

Глава 13. РАБОТА С КЛАССАМИ...154

Глава 14. УКАЗАТЕЛИ НА ОБЪЕКТЫ...167

Глава 15. ЗАЩИЩЁННЫЕ ЧЛЕНЫ КЛАССА: НЕ БЕСПОКОИТЬ!...181

Глава 16. СОЗДАНИЕ И УДАЛЕНИЕ ОБЪЕКТОВ...188

Глава 17. АРГУМЕНТАЦИЯ КОНСТРУИРОВАНИЯ...198

Глава 18. КОПИРУЮЩИЙ КОНСТРУКТОР...213

Глава 19. СТАТИЧЕСКИЕ ЧЛЕНЫ...224

В этой части...

Основным отличием С++ от других языков является возможность объектно-ориентированного программирования. Термин объектно-ориентарованный — один из самых популярных в современном компьютерном мире. Языки программирования, редакторы и базы данных — буквально все претендуют на звание объектно-ориентированных. Иногда так оно и есть, но часто такое определение даётся исключительно в рекламных целях.

На прилагаемом компакт-диске имеется программа BUDGET2, которая поможет вам разобраться в этих объектно-ориентированных концепциях.

Глава 11. ЗНАКОМСТВО С ОБЪЕКТНО-ОРИЕНТИРОВАННЫМ ПРОГРАММИРОВАНИЕМ...145

ОГЛАВЛЕНИЕ

В этой главе...

►Микроволновые печи и уровни абстракции 145

►Классификация микроволновых печей 146

►Зачем нужна классификация 147

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

►Микроволновые печи и уровни абстракции...145

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

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

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

■■■

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

■ Не смотрел внутрь печи.

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

■■■

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

_________________

145 стр. Глава 11. Знакомство с объектно-ориентированным программированием

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

Приготовление блюд с помощью функций...146

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

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

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

Приготовление "объектно-ориентированных" блюд...146

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

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

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

►Классификация, микроволновых печей...146

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

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

_________________

146 стр. Часть 3. Введение в классы

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

[Помни!]

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

Я не буду тратить место в книге на описание того, чем этот автомобиль похож на другие. Мне нужно знать лишь то, что это "машина, которая...", и то, чем она отличается от других машин ( например, ценой ). Теперь можно двигаться дальше. Легковые машины являются таким же подклассом колёсных транспортных средств, как грузовики и пикапы. При этом колёсные транспортные средства входят в состав транспортных средств наравне с кораблями и самолётами.

►Зачем нужна классификация...147

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

Иногда может показаться, что легче разработать и создать микроволновую печь специально для некоторого блюда и не строить универсальный прибор на все случаи жизни. Тогда на лицевую панель не надо будет помещать никаких кнопок, кроме кнопки СТАРТ. Блюдо всегда готовилось бы одинаковое время, и можно было бы избавиться от всех этих бесполезных кнопок типа РАЗМОРОЗКА или ТЕМПЕРАТУРА ПРИГОТОВЛЕНИЯ. Всё, что требовалось бы от такой печи, — это чтобы в неё помещалась одна тарелка с полуфабрикатом. Да, но что же тогда получится? Ведь при этом один кубический метр пространства использовался бы для приготовления всего одной тарелки закуски!

Чтобы сэкономить место, можно освободиться от этой глупой концепции — "микроволновая печь". Для приготовления закуски хватит и внутренностей печи. Тогда в инструкции достаточно написать примерно следующее: "Поместите полуфабрикат в ящик. Соедините красный и чёрный провод. Установите на трубе излучателя напряжение в 3000 вольт. Должен появиться негромкий гул. Постарайтесь не стоять близко к установке, если вы хотите иметь детей". Простая и понятная инструкция!

Но такой функциональный подход создаёт некоторые проблемы.

■■■

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

■ Не гибко. Когда-нибудь мне потребуется поменять свою микроволновую печь на печь другого типа. Я смогу это сделать без проблем, если интерфейс печи можно будет оставить старым. Без чётко очерченных областей действия, а также без разделения интерфейса и внутреннего содержимого становится крайне трудно убрать старый объект и поставить на его место новый.

_________________

147 стр. Глава 11. Знакомство с объектно-ориентированным программированием

■ Невозможно использовать повторно. Печи делаются для приготовления разных блюд. Мне, например, не хочется создавать новую печь всякий раз, когда требуется приготовить новое блюдо. Если задача уже решена, неплохо использовать её решение и в других программах.

■■■

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

_________________

148 стр. Часть 3. Введение в классы

Глава 12. КЛАССЫ В С++...149

ОГЛАВЛЕНИЕ

В этой главе...

►Введение в классы 149

►Формат класса 149

►Обращение к членам класса 150

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

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

►Введение в классы...149

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

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

►Формат класса...149

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

/* Класс dataset */

class NameDataSet

{

public :

char firstName[ 128 ] ;

char lastName [ 128 ] ;

int creditCard ;

} ;

/* Экземпляр класса dataset */

NameDataSet nds ;

_________________

149 стр. Глава 12. Классы в С++

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

После открывающей скобки находится ключевое слово public. ( Не спрашивайте меня сейчас, что оно значит, — я объясню его значение немного позже. В следующих главах поясняются разные ключевые слова, такие как public или private. А до тех пор пока я не сделаю private публичным, значение public останется приватным :-). )

«Можно использовать альтернативное ключевое слово struct, которое полностью идентично class, с предполагаемым использованием объявлений public.»

[Советы]

После ключевого слова public идёт описание полей класса. Как видно из листинга, класс NameDataSet содержит поля имени, фамилии и номера кредитной карты. Первые два поля являются символьными массивами, а третье имеет тип int ( будем считать, что это и есть номер кредитной карты ).

«Объявление класса содержит поля данных, необходимые для описания единого объекта.»

[Помни!]

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

Говорят, что nds является экземпляром класса NameDataSet и что мы создали этот экземпляр, реализовав класс NameDataSet. Поля firstName и остальные являются членами, или свойствами класса.

►Обращение к членам класса...150

Обратиться к членам класса можно так:

NameDataSet nds ;

nds.creditCard = 10 ;

cin >> nds.firstName ;

сin >> nds.lastName ;

Здесь nds — экземпляр класса NameDataSet ( или отдельный объект типа NameDataSet ); целочисленная переменная nds.creditCard — свойство объекта nds; член nds.creditCard имеет тип int, тогда как другой член этого объекта, nds.firstName, имеет тип char[ ].

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

Затем программа считывает имя и фамилию из стандартного ввода.

«Здесь я использую для хранения имени массив символов вместо типа string.»

[Помни!]

_________________

150 стр. Часть 3. Введение в классы

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

/* DataSet — хранение связанных данных в массиве объектов */

#include <cstdio>

#include <cstdlib>

#include <iostream>

#include <string.h>

using namespace std ;

/* NameDataSet — класс для хранения имени и номера кредитной карты */

class NameDataSet

{

public :

char firstName[ 128 ] ;

char lastName [ 128 ] ;

int creditCard ;

} ;

/* Прототипы функций: */

bool getData( NameDataSet& nds ) ;

void displayData( NameDataSet& nds ) ;

int main( int nNumberofArgs , char* pszArgs[ ] )

{

/* печать кириллицы если Вы не используете программки gccrus.exe и g++rus.exe */

setlocale ( LC_ALL , ".1251" ) ;

/* Выделяем память для 25 экземпляров */

const int MAX = 25 ;

NameDataSet nds[ MAX ] ;

/* Загружаем имя, фамилию и номер социального страхования */

cout << "Считываем информацию о пользователе\n"

<< "Введите 'exit' для выхода из программы"

<< endl ;

int index = 0 ;

while ( getData( nds[ index ] ) && index < MAX)

{

index++ ;

}

/* Выводим считанные имя и номер */

cout << "\nЗаписи:" << endl ;

for ( int i = 0 ; i < index ; i++ )

{

displayData( nds[ i ] ) ;

}

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

/* getData — заполнение объекта информацией */

bool getData( NameDataSet& nds )

_________________

151 стр. Глава 12. Классы в С++

{

cout << "\nВведите имя:" ;

cin >> nds.firstName ;

/* Проверяем, не пора ли выйти из программы */

if ( stricmp( nds.firstName , "exit" ) == 0 )

{

return false ;

}

cout << "Введите фамилию:" ;

cin >> nds.lastName ;

cout << "Введите номер кредитной карты:" ;

cin >> nds.creditCard ;

return true ;

}

/* displayData — Вывод набора данных */

void displayData( NameDataSet& nds )

{

cout << nds.firstName

<< " "

<< nds.lastName

<< " /"

<< nds.creditCard

<< endl ;

}

В функции main( ) создаётся массив из 25 объектов класса NameDataSet, после чего программа приглашает пользователя ввести необходимую информацию. Затем в теле цикла while происходит вызов функции getData( ), которая ожидает ввода с клавиатуры содержимого элементов массива. Цикл прерывается, если getData( ) возвращает false или если количество заполненных объектов достигло максимального значения ( в данном случае — 25 ). После этого созданные объекты передаются функции displayData, которая выводит их на экран.

Функция getData( ) принимает аргумент типа NameDataSet, которому внутри функции присваивается имя nds. Пока что не обращайте внимания на символ "&" — о нём речь пойдёт в главе 14, "Указатели на объекты".

Внутри функции getData( ) происходит считывание строки из устройства стандартного ввода с последующей его записью в член firstName. Если stricmp( ) находит, что введённая строка — "exit", функция getData( ) возвращает false функции main( ), сигнализируя, что пора выходить из цикла ввода информации. ( Функция stricmp( ) сравнивает строки, не обращая внимания на регистр. Строки "EXIT", "exit" и другие считаются идентичными. ) Если введена строка, отличная от "exit", функция считывает из стандартного ввода фамилию и номер кредитной карты и записывает их в объект nds. Функция displayData( ) выводит на экран все члены объекта nds. Результат работы этой программы выглядит следующим образом.

Считываем информацию о пользователе

Введите 'exit' для выхода из программы

Введите имя: Stephen

Введите фамилию: Davis

Введите номер кредитной карты: 123456

_________________

152 стр. Часть 3. Введение в классы

Введите имя: Marshall

Введите фамилию: Smith

Введите номер кредитной карты: 567890

Введите имя: exit

Записи:

Stephen Davis/123456

Marshall Smith/567890

Для продолжения нажмите любую клавишу...

Вывод программы начинается с пояснения, как с ней работать. В первой строке я ввёл своё имя ( видите, какой я скромный! ). Поскольку меня не зовут exit, программа продолжает выполнение. Далее я ввёл свою фамилию и номер кредитной карты. Следующим элементом массива я ввёл имя Marshall Smith и номер его кредитной карты. Затем я ввёл строку exit и таким образом прервал цикл заполнения объектов. Как видите, эта программа не делает ничего, кроме вывода только что введённой информации.

_________________

153 стр. Глава 12. Классы в С++

Глава 13. РАБОТА С КЛАССАМИ...154

ОГЛАВЛЕНИЕ

В этой главе...

►Активизация объектов 154

►Добавление функции-члена 156

►Вызов функций-членов 157

►Разрешение области видимости 161

►Определение функции-члена 162

►Определение функций-членов вне класса 164

►Перегрузка функций-членов 165

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

class Savings

{

public :

unsigned accountNumber ;

float balance ;

} ;

Каждый экземпляр класса Savings содержит одинаковые элементы:

void fn( void )

{

Savings a ;

Savings b ;

a.accountNumber = 1 ; /* этот счёт не тот же, что и... */

b.accountNumber = 2 ; /* ...этот */

}

Переменная а.accountNumber отличается от переменной b.accountNumber. Эти переменные различаются между собой так же, как баланс моего банковского счёта отличается от вашего ( хотя они оба называются балансами ).

►Активизация объектов...154

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

_________________

154 стр. Часть 3. Введение в классы

Моделирование реальных объектов...155

Реальные объекты имеют свойства-данные, например номера счетов и балансы. Но кроме этого, реальные объекты могут выполнять действия: микроволновые печи готовят, сберегательный счёт начисляет проценты, полицейский выписывает штраф и т.д.

Функционально ориентированные программы выполняют все необходимые действия с помощью функций. Программа на С++ может вызвать функцию strcmp( ) для сравнения двух строк или функцию getLine( ) для ввода строки. В главе 24, "Использование потоков ввода-вывода", будет показано, что даже операторы работы с потоками ввода-вывода ( cin >> и cout << ) являются не чем иным, как особым видом вызова функции.

Для выполнения действий классу Savings необходимы собственные активные свойства:

class Savings

{

public :

unsigned deposit( unsigned amount )

{

balance += amount ;

return balance ;

}

unsigned int accountNumber ;

float balance ;

} ;

В приведённом примере помимо номера и баланса счёта в класс Savings добавлена функция deposit( ). Теперь класс Savings может самостоятельно управлять своим состоянием. Так же, как класс MicrowaveOven ( микроволновая печь ) содержит функцию cook( ) ( готовить ), класс Savings содержит функцию deposit( ). Функции, определённые в классе, называются функциями-членами.

Зачем нужны функции-члены...155

Почему мы должны возиться с функциями-членами? Что плохого в таком фрагменте:

class Savings

{

public :

unsigned accountNumber ;

float balance ;

} ;

unsigned deposit( Savings& s , unsigned amount )

{

s.balance += amount ;

return s.balance ;

}

Ещё раз напомню: пока что не обращайте внимания на символ "&" — его смысл станет понятен позже.

В этом фрагменте deposit( ) является функцией "вклада на счёт". Эта функция поддержки реализована в виде внешней функции, которая выполняет необходимые действия с экземпляром класса Savings. Конечно, такой подход имеет право на существование, но он нарушает наши правила объектно-ориентированного программирования.

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

_________________

155 стр. Глава 13. Работа с классами

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

►Добавление функции-члена...156

Эта процедура включает два аспекта: создание функции-члена и её именование ( звучит довольно глупо, не правда ли? ).

Создание функции-члена...156

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

class Student

{

public :

/* Добавить пройденный курс к записи */

float addCourse( int hours , float grade )

{

/* Вычислить среднюю оценку с учётом времени различных курсов */

float weightedGPA ;

weightedGPA = semesterHours * gpa ;

/* Добавить новый курс */

semesterHours += hours ;

weightedGPA += grade * hours ;

gpa = weightedGPA / semesterHours ;

/* Вернуть новую оценку */

return gpa ;

}

int semesterHours ;

float gpa ;

} ;

Функция addCourse( int , float ) является функцией-членом класса Student. По сути, это такое же свойство класса Student, как и свойства semesterHours и gpa.

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

«Функции-члены не обязаны предшествовать членам-данным в объявлении класса, как сделано в приведённом выше примере. Члены класса могут быть перечислены в любом порядке. Просто я предпочитаю первыми размещать функции.»

[Советы]

_________________

156 стр. Часть 3. Введение в классы

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

[Технические подробности]

Именование членов класса...157

Функция-член во многом похожа на члена семьи. Полное имя нашей функции addCourse( int , float ) пишется как Student::addCourse( int , float ), так же как моё полное имя — Стефан Дэвис. Краткое имя этой функции — addCourse( int , float ), а моё краткое имя — Стефан. Имя класса в начале полного имени означает, что эта функция является членом класса Student ( :: между именами функции и класса является просто символом-разделителем ). Фамилия Дэвис после моего имени означает, что я являюсь членом семьи Дэвисов.

«Существует и другое название полного имени — расширенное имя.»

[Советы]

Мы можем определить функцию addCourse( int , float ), которая не будет иметь ничего общего с классом Student ; точно так же, как могут существовать люди с именем Стефан, которые не имеют ничего общего с моей семьёй ( это можно понимать и дословно: я знаю несколько Стефанов, которые не хотят иметь ничего общего с моей семьёй ).

Вы можете создать функцию с полным именем Teacher::addCourse( int , float ) или даже с именем Golf::addCourse( int , float ). Имя addCourse( int , float ) без имени класса означает, что это обычная функция, которая не является членом какого-либо класса.

«Расширенное имя функции, не являющейся членом какого-либо класса, имеет вид ::addCourse( int , float ).»

[Советы]

►Вызов функций-членов...157

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

class Student

{

public :

int semesterHours ;

float gpa ;

} ;

Student s ;

void fn( void )

{

/* Обращение к данным-членам объекта s */

s.semesterHours = 10 ;

s.gpa = 3.0 ;

}

_________________

157 стр. Глава 13. Работа с классами

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

Student s ;

void fn( void )

{

/* Этот пример ошибочен */

semesterHours = 10 ;

/* Член какого объекта и какого класса? */

Student::semesterHours = 10 ;

/* Теперь ясно, какого класса, однако до сих пор не ясно, какого объекта */

}

Обращение к функциям-членам...158

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

/* CallMemberFunction — определение и вызов */

/*                         функции-члена класса */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

class Student

{

public :

/* Добавить пройденный курс к записи */

float addCourse( int hours , float grade )

{

/* Вычислить среднюю оценку с учётом времени различных курсов */

float weightedGPA ;

weightedGPA = semesterHours * gpa ;

/* Добавить новый курс */

semesterHours += hours ;

weightedGPA += grade * hours ;

gpa = weightedGPA / semesterHours ;

/* Вернуть новую оценку */

return gpa ;

}

int semesterHours ;

float gpa ;

} ;

int main( int nNumberofArgs , char* pszArgs[ ] )

_________________

158 стр. Часть 3. Введение в классы

{

Student s ;

s.semesterHours = 10 ;

s.gpa = 3.0 ;

/* Значения до вызова */

cout << "До: s = ( " << s.semesterHours

<< ", " << s.gpa

<< " )" << endl ;

s.addCourse( 3 , 4.0 ) ; /* Вызов функции-члена */

/* Изменённые значения */

cout << "После: s = ( " << s.semesterHours

<< ", " << s.gpa

<< " )" << endl ;

/* Обращение к другому объекту */

Student t ;

t.semesterHours = 6 ;

t.gpa = 1.0 ;

t.addCourse( 3 , 1.5 ) ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

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

Факт вызова этой функции можно определить так: "s является объектом, на который действует addCourse( ) " ; или, другими словами, объект s представляет собой студента, к записи которого добавляется новый курс. Вы не можете получить информацию о студенте или изменить её, не указав, о каком конкретно студенте идёт речь.

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

Доступ к членам из функции-члена...159

Я так и слышу, как вы повторяете про себя: "Нельзя обратиться к функции-члену без указания имени объекта! Нельзя обратиться к функции-члену без указания имени объекта! Нельзя..." Запомнив это, вы смотрите на тело функции-члена Student::addCourse( ) и... что это? Ведь addCourse( ) обращается к членам класса, не уточняя имени объекта!

Возникает вопрос: так всё-таки можно или нельзя обратиться к члену класса, не указывая его объекта? Уж поверьте мне, что нельзя. Просто когда вы обращаетесь к члену класса Student из addCourse( ), по умолчанию используется тот экземпляр класса, из которого вызвана функция addCourse( ). Вы ничего не поняли? Вернёмся к примеру.

int main( int nNumberofArgs , char* pszArgs[ ] )

{

Student s ;

s.semesterHours = 10 ;

s.gpa = 3.0 ;

s.addCourse( 3 , 4.0 ) ; /* Вызов функции-члена */

Student t ;

t.semesterHours = 6;

t.gpa = 1.0 ;

t.addCourse( 3 , 1.5 ) ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

_________________

159 стр. Глава 13. Работа с классами

Когда addCourse( ) вызывается для объекта s, все сокращённые имена в теле этой функции считаются членами объекта s. Таким образом, обращение к переменной semesterHours внутри функции s.addCourse( ) в действительности является обращением к переменной s.semesterHours, а обращение к gpa — обращением к s.gpa. В следующей строке функции main( ), когда addCourse( ) вызывается для объекта t того же класса Student, происходит обращение к членам класса t.semesterHours и t.gpa.

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

[Помни!]

Именование текущего объекта...160

«Как функция-член определяет, какой объект является текущим? Это не магия и не шаманство — просто адрес этого объекта всегда передаётся функции-члену как скрытый первый аргумент. Другими словами, при вызове функции-члена происходит преобразование такого вида:

s.addCourse( 3 , 2.5 ) равносильно Student::addCourse( &s , 3 , 2.5 )

( команда, приведённая в правой части выражения, синтаксически неверна; она просто показывает, как компилятор видит выражение в левой части во внутреннем представлении ).»

[Технические подробности]

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

float Student::addCourse( int hours , float grade )

{

float weightedGPA ;

weightedGPA = this -> semesterHours * this -> gpa ;

/* добавим новый курс */

this -> semesterHours += hours ;

weightedGPA += hours * grade ;

this -> gpa = weightedGPA / this -> semesterHours ;

return this -> gpa ;

}

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

_________________

160 стр. Часть 3. Введение в классы

►Разрешение области видимости...161

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

С помощью оператора :: можно также описать функцию — не член, использовав для этого пустое имя класса. В этом случае функция addCourse( ) должна быть описана как ::addCourse( int , float ).

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

/* addCourse — перемножает количество часов и оценку */

float addCourse( int hours , float grade )

{

return hours * grade ;

}

class Student

{

public :

int semesterHours ;

float gpa ;

/* Добавить пройденный курс к записи */

float addCourse( int hours , float grade )

{

/* Вызвать внешнюю функцию */

weightedGPA = addCourse( semesterHours , gpa ) ;

/* Вызвать ту же функцию для подсчёта оценки с  учётом нового курса */

weightedGPA += addCourse( hours , grade ) ;

gpa = weightedGPA / semesterHours ;

/* Вернуть новую оценку */

return gpa ;

}

} ;

В этом фрагменте я хотел, чтобы функция-член Student::addCourse( ) вызывала функцию — не член ::addCourse( ). Без оператора :: вызов функции addCourse( ) внутри класса Student приведёт к вызову функции Student::addCourse( ).

«Функция-член может использовать для обращения к другому члену класса сокращённое имя, подразумевающее использование имени текущего экземпляра класса.»

[Помни!]

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

/* addCourse — перемножает количество часов и оценку */

float addCourse( int hours , float grade )

{

return hours*grade ;

}

_________________

161 стр. Глава 13. Работа с классами

class Student

{

public :

int semesterHours ;

float gpa ;

/* Добавить пройденный курс к записи */

float addCourse( int hours , float grade )

{

/* Вызвать внешнюю функцию */

weightedGPA = ::addCourse( semesterHours , gpa ) ;

/* Вызвать ту же функцию для подсчёта оценки с учётом нового курса */

weightedGPA += ::addCourse( hours , grade ) ;

gpa = weightedGPA / semesterHours ;

/* Вернуть новую оценку */

return gpa ;

}

} ;

Это похоже на то, как если бы я звал Стефана в собственном доме. Все решили бы, что я зову самого себя: ведь в моём доме, естественно, подразумевается фамилия Дэвис. Если же я имею в виду какого-то другого Стефана, то должен сказать "Стефан Спупендайк" или "Стефан Мак-Суини" либо использовать какую-нибудь другую фамилию. Так же действует и оператор разрешения области видимости.

«Расширенное имя функции включает в себя её аргументы. Теперь же мы добавляем к полному имени ещё и имя класса, к которому принадлежит функция.»

[Помни!]

►Определение функции-члена...162

Функция-член может быть определена как внутри класса, так и отдельно от него. Когда функция определяется внутри класса, это выглядит так, как в приведённом далее файле Savings.h:

/* Savings — определение класса с возможностью делать вклады */

class Savings

{

public :

/* Объявляем и определяем функции-члены */

float deposit( float amount )

{

balance += amount ;

return balance ;

}

unsigned int accountNumber ;

float balance ;

} ;

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

_________________

162 стр. Часть 3. Введение в классы

/* SavingsClassInline — вызов фукции-члена, объявленной и определённой в классе Savings */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

#include " Savings.h "

int main( int nNumberofArgs , char* pszArgs[ ] )

{

/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */

setlocale (LC_ALL,".1251");

Savings s ;

s.accountNumber = 123456 ;

s.balance = 0.0 ;

/* Добавляем немного на счёт... */

cout << "Вкладываем на счёт 10 монет"

<< s.accountNumber << endl ;

s.deposit( 10 ) ;

cout << "Состояние счёта "

<< s.balance << endl ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

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

«Директива #include заставляет препроцессор перед началом компиляции вставить вместо неё содержимое указанного в ней файла.»

[Помни!]

«Встраиваемые функции-члены

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

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

[Технические подробности]

_________________

163 стр. Глава 13. Работа с классами

►Определение функций-членов вне класса...164

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

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

/* Savings — определение класса с */

/*         возможностью делать вклады */

class Savings

{

public :

/* Объявляем, но не определяем функции-члены */

float deposit( float amount ) ;

unsigned int accountNumber ;

float balance ;

} ;

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

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

[Советы]

/* SavingsClassOutline — вызов фукции-члена, */

/*               объявленной   в классе Savings ( заголовочном файле ), но определённой */

/*               в программе    SavingsClassOutline или  */

/*      тело функции находится в отдельном исходном файле */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

#include " Savings.h "

/* Определение функции-члена Savings::deposit( ) ( обычно содержится в отдельном файле ) */

float Savings::deposit( float amount )

{

balance += amount ;

return balance ;

}

/* Основная программа */

int main( int nNumberofArgs , char* pszArgs[ ] )

{

/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */

setlocale (LC_ALL,".1251");

Savings s ;

s.accountNumber = 123456 ;

s.balance = 0.0 ;

_________________

164 стр. Часть 3. Введение в классы

/* Добавляем немного на счёт... */

cout << "Вкладываем на счёт 10 монет"

<< s.accountNumber << endl ;

s.deposit( 10 ) ;

cout << "Состояние счёта "

<< s.balance << endl ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

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

Обратите внимание, что при определении функции-члена deposit( ) потребовалось указание её полного имени

float Savings::deposit( float amount )

; сокращённого имени при определении вне класса недостаточно.

►Перегрузка функций-членов...165

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

class Student

{

public :

/* grade — возвращает текущую среднюю оценку */

float grade( ) ;

/* grade — устанавливает новое значение оценки и возвращает предыдущее */

float grade( float newGPA )

/* ...прочие члены-данные... */

} ;

class Slope

{

public :

/* grade — возвращает снижение оценки */

float grade( ) ;

/* ...прочие члены-данные... */

} ;

/* grade — возвращает символьный эквивалент оценки */

char grade( float value ) ;

int main( int argcs , char* pArgs[ ] )

{

Student s ;

s.grade( 3.5 ) ; /* Student::grade( float ) */

float v = s.grade( ) ; /* Student::grade( ) */

char с = grade( v ) ; /* ::grade( float ) */

Slope o ;

float m = о.grade( ) ; /* Slope::grade( ) */

return 0 ;

}

_________________

165 стр. Глава 13. Работа с классами

Полные имена вызываемых из main( ) функций указаны в комментариях.

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

В приведённом примере первые два вызова обращаются к функциям-членам Student::grade( float ) и Student::grade( ) соответственно. Эти функции отличаются списками аргументов. Вызов функции s.grade( ) обращается к Student::grade( ), поскольку тип объекта s — Student.

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

_________________

166 стр. Часть 3. Введение в классы

Глава 14. УКАЗАТЕЛИ НА ОБЪЕКТЫ...167

ОГЛАВЛЕНИЕ

В этой главе...

►Определение массивов и указателей 167

►Объявление массивов объектов 168

►Объявление указателей на объекты 169

►Передача объектов функциям 171

►Зачем использовать указатели и ссылки 174

►Возврат к куче 175

►Сравнение указателей и ссылок 175

►Почему ссылки не используются вместо указателей 175

►Использование связанных списков 176

►Списки в стандартной библиотеке 180

Программисты на С++ всё время создают массивы чего-либо. Формируются массивы целочисленных значений, массивы действительных значений; так почему бы не создать массив студентов? Студенты всё время находятся в списках ( причём гораздо чаще, чем им хотелось бы ). Концепция объектов Student, стройными рядами ожидающих своей очереди, слишком привлекательна, чтобы можно было пройти мимо неё.

►Определение массивов и указателей...167

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

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

int array[ 10 ] ; /* Объявление массива из 10 элементов */

К отдельному элементу массива можно обратиться, подсчитав смещение от начала массива:

array[ 0 ] = 10 ; /* Присвоить 10 первому элементу */

array[ 9 ] = 20 ; /* Присвоить 20 последнему элементу */

В этом фрагменте первому элементу массива ( элементу под номером 0 ) присваивается значение 10 , а последнему — 20.

«Не забывайте, что в С++ массив начинается элементом с индексом 0 и заканчивается элементом, имеющим индекс, равный длине массива минус 1.»

[Помни!]

_________________

167 стр. Глава 14. Указатели на объекты

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

/* Объявление целочисленной переменной */

int variable ;

/* Сохранить её адрес в pVariable */

int* pVariable = &variable

/* Присвоить 10  целочисленной переменной,  на которую указывает pVariable */

*pVariable = 10 ;

Указатель pVariable был объявлен для того, чтобы хранить в нём адрес переменной variable. После этого целочисленной переменной, находящейся по адресу pVariable, присваивается значение 10.

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

■■■

■ variable — это дом;

■ pVariable — это листок с адресом дома;

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

■■■

В главе 7, "Хранение последовательностей в массивах", описаны основы работы с массивами простых ( встроенных ) типов, а в главах 8, "Первое знакомство с указателями в С++", и 9, "Второе знакомство с указателями", подробно рассматриваются указатели.

►Объявление массивов объектов...168

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

/* ArrayOfStudents — определение массива */

/*             объектов Student и обращение */

/*               к его элементам */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

class Student

{

public :

int  semesterHours ;

float gpa ;

float addCourse( int hours , float grade ){ return 0.0 ; }

} ;

void someFn( )

{

/* Объявляем массив из 10 студентов */

Student s[ 10 ] ;

/* Пятый студент получает 5.0 ( повезло! ) */

s[ 4 ].gpa = 5.0 ;

/* Добавим ещё один курс пятому студенту, который на этот раз провалился... */

s[ 4 ].addCourse( 3 , 0.0 ) ;

}

int main( int nNumberofArgs , char* pszArgs[ ] )

{

system( "PAUSE" ) ;

return 0 ;

}

_________________

168 стр. Часть 3. Введение в классы

В данном фрагменте s является массивом объектов типа Student. Запись s[ 4 ] означает пятый элемент массива, а значит, s[ 4 ].gpa является усреднённой оценкой пятого студента. В следующей строке с помощью функции s[ 4 ].addCourse( ) пятому студенту добавляется ещё один прослушанный и несданный курс.

►Объявление указателей на объекты...169

Указатели на объекты работают так же, как и указатели на простые типы.

/* ObjPtr — Определение и использование */

/*              указателя на объект Student */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

class Student

{

public :

int semesterHours ;

float gpa ;

float addCourse( int hours , float grade ) { return 0.0 ; } ;

} ;

int main( int argc , char* pArgs[ ] )

{

/* Создание объекта Student */

Student s ;

s.gpa = 3.0 ;

/* Создание указателя на объект Student */

Student* pS ;

/* Заставляем указатель указывать на наш объект */

pS = &s ;

cout << "s.gpa     = " << s.gpa << "\n"

<< "pS -> gpa = " << pS -> gpa << endl ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

В программе объявляется переменная s типа Student, после чего создаётся переменная pS, которая является "указателем на объект типа Student" ; другими словами, указателем Student*. Программа инициализирует значение одного из членов-данных s, и присваивает адрес s переменной pS. Затем программа обращается к объекту s — один раз по имени, а затем с использованием указателя на объект. Странную запись pS -> gpa я объясню немного позже в этой главе.

Разыменование указателей на объекты...169

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

_________________

169 стр. Глава 14. Указатели на объекты

int main( int argc , char* pArgs[ ] )

{

/* Этот пример некорректен */

Student s ;

Student* pS= &s ; /* Создаём указатель на объект s */

/* Обращаемся к члену gpa объекта, на который указывает pS ( этот фрагмент неверен ) */

*pS.gpa = 3.5 ;

return 0 ;

}

Как верно сказано в комментарии, этот код работать не будет. Проблема в том, что оператор "." будет выполнен раньше оператора "*".

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

int i = 2 * ( 1 + 3 ) ; /* сложение выполняется до умножения */

В применении к указателям скобки выполняют те же функции.

int main( int argc , char* pArgs[ ] )

{

Student s ;

Student* pS = &s ; /* Создаём указатель на объект s */

/* Обращаемся к члену gpa того объекта, на который указывает pS ( теперь всё работает правильно ) */

( *pS ).gpa = 3.5 ;

return 0 ;

}

Теперь *pS вычисляет объект, на который указывает pS, а следовательно, .gpa обращается к члену этого объекта.

Использование стрелок...170

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

Для доступа к членам объекта С++ предоставляет более удобный оператор  -> , позволяющий избежать неуклюжей конструкции со скобками и оператором *; таким образом, pS -> gpa эквивалентно ( *pS ).gpa. В результате получаем следующий преобразованный код рассмотренной ранее программы.

int main( int argc , char* pArgs[ ] )

{

Student s ;

Student* pS = &s ; /* Создаём указатель на объект s */

/* Обращаемся к члену gpa того объекта, на который указывает pS ( теперь всё работает правильно ) */

pS -> gpa = 3.5 ;

return 0 ;

}

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

_________________

170 стр. Часть 3. Введение в классы

►Передача объектов функциям...171

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

Вызов функции с передачей объекта по значению...171

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

/* PassObjVal — попытка изменить значение объекта в функции оказывается неуспешной при передаче объекта по значению */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

class Student

{

public :

int semesterHours ;

float gpa ;

} ;

void someFn( Student copyS )

{

copyS.semesterHours = 10 ;

copyS.gpa = 3.0 ;

cout << "Значение copyS.gpa = "

<< copyS.gpa << "\n" ;

}

int main( int argc , char* pArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

Student s ;

s.gpa = 0.0 ;

/* Вывод значения s.gpa до вызова someFn( ) */

cout << "Значение s.gpa = " << s.gpa << "\n" ;

/* Передача существующего объекта */

cout << "Вызов someFn( Student )\n" ;

someFn( s ) ;

cout << "Возврат из someFn( Student )\n" ;

/* Значение s.gpa остаётся равным 0 */

cout << "Значение s.gpa = " << s.gpa << "\n" ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

_________________

171 стр. Глава 14. Указатели на объекты

В этом примере функция main( ) создаёт объект s, а затем передаёт его в функцию someFn( ).

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

[Помни!]

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

Значение s.gpa = 0

Вызов someFn( Student )

Значение copyS.gpa = 3

Возврат из someFn( Student )

Значение s.gpa = 0

Press any key to continue...

Вызов функции с передачей указателя...172

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

/* PassObjPtr — изменение значения объекта в функции при передаче указателя на объект */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

class Student

{

public :

int semesterHours ;

float gpa ;

} ;

void someFn( Student* pS )

{

pS -> semesterHours = 10 ;

pS -> gpa           = 3.0 ;

cout << "Значение pS -> gpa = "

<< pS -> gpa << "\n" ;

}

int main( int argc , char* pArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

Student s ;

s.gpa = 0.0 ;

/* Вывод значения s.gpa до вызова someFn( ) */

cout << "Значение s.gpa = " << s.gpa << "\n" ;

/* Передача существующего объекта */

cout << "Вызов someFn( Student* )\n" ;

someFn( &s ) ;

cout << "Возврат из someFn( Student* )\n" ;

/* Значение s.gpa теперь равно 3.0 */

cout << "Значение s.gpa = " << s.gpa << "\n" ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

_________________

172 стр. Часть 3. Введение в классы

В этом примере аргумент, передаваемый в someFn( ), имеет тип указателя на объект Student, что записывается как Student* ( это отражает способ вызова программой функции someFn( ) ). Теперь вместо значения объекта s в функцию someFn( ) передаётся указатель на объект s. При этом соответственно изменяется и способ обращения к аргументам функции внутри её тела: теперь для разыменования указателя pS используются операторы-стрелки.

На этот раз вывод программы имеет следующий вид.

Значение s.gpa = 0

Вызов someFn( Student* )

Значение pS -> gpa = 3

Возврат из someFn( Student* )

Значение s.gpa = 3

Press any key to continue...

Передача объекта no ссылке...173

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

/* PassObjRef — изменение значения объекта в функции при передаче с использованием ссылки */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

class Student

{

public :

int semesterHours ;

float gpa ;

} ;

void someFn( Student& refS )

{

refS.semesterHours = 10 ;

refS.gpa = 3.0 ;

cout << "Значение refS.gpa = "

<< refS.gpa << "\n" ;

}

int main( int argc , char* pArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

Student s ;

s.gpa = 0.0 ;

_________________

173 стр. Глава 14. Указатели на объекты

/* Вывод значения s.gpa до вызова someFn( ) */

cout << "Значение s.gpa =  " << s.gpa << "\n" ;

/* Передача существующего объекта */

cout << "Вызов someFn( Student& )\n" ;

someFn( s ) ;

cout << "Возврат из someFn ( Student& )\n" ;

/* Значение s.gpa теперь равно 3.0 */

cout << "Значение s.gpa = " << s.gpa << "\n" ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

Значение s.gpa = 0

Вызов someFn( Student& )

Значение refS.gpa = 3

Возврат из someFn( Student& )

Значение s.gpa = 3

Press any key to continue...

В этой программе в функцию someFn( ) передаётся не копия объекта, а ссылка на него. Изменения, внесённые функцией someFn( ) в s, сохраняются внутри main( ).

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

[Советы]

►Зачем, использовать указатели и ссылки...174

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

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

А вот и вторая причина — некоторые объекты могут оказаться действительно очень большими. Передача такого объекта по значению приводит к копированию большого объёма информации в память функции.

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

[Технические подробности]

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

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

[Атас!]

_________________

174 стр. Часть 3. Введение в классы

►Возврат к куче...175

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

MyClass* myFunc( )

{

/* Эта функция не будет работать правильно */

MyClass mc ;

MyClass* рМС = &mc ;

return рМС ;

}

После возврата из myFunc( ) объект mc выходит из области видимости, а значит, указатель, который возвращает myFunc( ), указывает на несуществующий объект.

«Проблемы, связанные с возвратом памяти, которая выходит из области видимости, рассматривались в главе 9, "Второе знакомство с указателями".»

[Помни!]

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

MyClass* myFunc( )

{

MyClass* рМС = new MyClass ;

return рМС ;

}

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

[Помни!]

►Сравнение указателей и ссылок...175

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

«В принципе, можно обойтись чем-то одним. Тот же С#, да и многие другие языки обходятся без указателей. Однако С++ — язык крайне широкого применения, и имеется множество задач, решение которых существенно упрощается при использовании указателей. Указатели — неотъемлемая часть стандартного, не ограниченного узкими рамками Visual Studio .NET языка программирования С++.»

[Советы]

►Почему ссылки не используются вместо указателей...175

Синтаксис работы со ссылками аналогичен синтаксису, используемому при работе с обычными объектами. Так почему бы не перейти на использование только ссылок и никогда не использовать указатели?

_________________

175 стр. Глава 14. Указатели на объекты

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

class Student

{

public :

int semesterHours ;

float gpa ;

Student valFriend ;

Student& refFriend ;

Student* ptrFriend ;

} ;

int main( int nNumberOfArgs , char* pszArgs[ ] )

{

/* Ссылка на объект в куче */

Student& student = *new Student ;

student.gpa = 10 ;

// To же

Student& studentFriend = *new Student ;

studentFriend.gpa = 20 ;

/* Копирование значения одного объекта типа Student в другой */

student.valFriend = studentFriend ;

/* Этот код не будет работать */

Student& refFriend ;

refFriend = studentFriend ;

/* Этот код корректен */

student.ptrFriend = &studentFriend ;

return 0 ;

}

Как видите, я модифицировал класс Student так, чтобы он мог указать своего лучшего друга[ 14 ]. Для этого я пытаюсь воспользоваться ссылочной переменной. В функции main( ) я создаю двух студентов и пытаюсь сделать одного из них другом другого.

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

►Использование связанных списков...176

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

___________

14Это сделано некорректно; как минимум член valFriend не может быть определён в классе того же типа, не считая массы других ошибок. Поэтому к данному примеру следует относиться как к не более чем поясняющей сугубо теоретической модели, которая никогда не будет даже скомпилирована. — Прим. ред.

_________________

176 стр. Часть 3. Введение в классы

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

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

class LinkableClass

{

public :

LinkableClass* pNext ;

/* Прочие члены класса */

} ;

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

Указатель pNext и есть тот элемент, с помощью которого дети объединяются в цепочки. Фигурально выражаясь, можно сказать, что список детей состоит из некоторого количества объектов, каждый из которых имеет тип "ребёнок". Каждый ребёнок указывает на следующего ребенка.

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

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

[Атас!]

«Преобразование целочисленного нуля в тип LinkableClass* не обязательно. С++ воспринимает 0 как значение любого типа ( в частности, как "универсальный указатель" ).»

[Советы]

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

void addHead( LinkableClass* pLC )

{

pLC -> pNext = pHead

pHead = pLC ;

}

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

Другие операции над связанным списком...177

Добавление объекта в начало списка — самая простая операция со связанным списком. Хорошее представление о работе связанного списка даёт процедура прохода по нему до конца списка.

_________________

177 стр. Глава 14. Указатели на объекты

/* Проход по связанному списку */

LinkableClass* pL = pHead ;

while ( pL )

{

/* Выполнение некоторых операций */

/* Переход к следующему элементу */

pL = pL -> pNext ;

}

Сначала указатель pL инициализируется адресом первого объекта в списке ( который хранится в переменной pHead ). Затем программа входит в цикл while. Если указатель pL не нулевой, он указывает на некоторый объект LinkableClass. В этом цикле программа может выполнить те или иные действия над объектом, после чего присвоение pL = pL -> pNext "перемещает" указатель к следующему объекту списка. Если указатель становится нулевым, список исчерпан.

Программа LinkedListData...178

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

/* LinkedListData — хранение данных в связанном списке */

#include <cstdio>

#include <cstdlib>

#include <iostream>

#include <string.h>

using namespace std ;

/* NameDataSet — хранит имя человека ( этот объект можно легко расширить для хранения другой информации ). */

class NameDataSet

{

public :

char szName[ 128 ] ;

/* Указатель на следующую запись в списке */

NameDataSet* pNext ;

} ;

/* Указатель на первую запись списка */

NameDataSet* pHead = 0 ;

/* Добавление нового члена в список */

void add( NameDataSet* pNDS )

{

pNDS -> pNext = pHead ;

/* Заголовок указывает на новую запись */

pHead = pNDS ;

}

/* getData — чтение имени */

NameDataSet* getData( )

{

_________________

178 стр. Часть 3. Введение в классы

// Читаем имя

char nameBuffer [ 128 ] ;

cout << "\nВведите имя:" ;

cin >> nameBuffer ;

/* Если это имя — 'exit'... */

if ( ( stricmp( nameBuffer , "exit" ) == 0 ) )

{

/* ...вернуть нулевое значение */

return 0 ;

}

/* Новая запись для заполнения */

NameDataSet* pNDS = new NameDataSet ;

/* Заполнение поля имени и обнуление указателя */

strncpy( pNDS -> szName , nameBuffer , 128 ) ;

pNDS -> szName[ 127 ] = '\0' ;

pNDS -> pNext = 0 ;

/* Возврат адреса созданного объекта */

return pNDS ;

}

int main( int nNumberofArgs , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

cout << "Читаем имена студентов\n"

<< "Введите 'exit' для выхода\n" ;

/* Создание объекта NameDataSet */

NameDataSet* pNDS ;

while ( pNDS = getData( ) )

{

/* Добавление в конец списка */

add( pNDS ) ;

}

/* Итерация списка для вывода записей */

cout << "Записи:\n" ;

pNDS = pHead ;

while ( pNDS )

{

/* Вывод текущей записи */

cout << pNDS -> szName << "\n" ;

/* Получение следующей записи */

pNDS = pNDS -> pNext ;

}

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

Несмотря на внушительную длину, программа LinkedListData относительно проста. Функция main( ) начинается с вызова функции getData( ), которая считывает элемент NameDataSet с клавиатуры. Если пользователь вводит строку "exit", getData( ) возвращает нуль. Функция main( ) вызывает функцию add( ), чтобы добавить элемент, который вернула getData( ), в конец связанного списка.

_________________

179 стр. Глава 14. Указатели на объекты

Если от пользователя больше не поступает элементов NameDataSet, функция main( ) выводит на экран все элементы списка, используя функцию displayData( ).

Функция getData( ) выделяет из кучи пустой объект класса NameDataSet. После этого getData( ) ожидает ввода имени для записи его в соответствующее поле нового объекта. Если пользователь вводит в поле имени строку "exit", функция уничтожает последний созданный объект и возвращает 0. В противном случае getData( ) считывает фамилию и номер социального страхования, после чего обнуляет указатель pNext и передаёт управление вызывающей функции.

«Никогда не оставляйте связывающие указатели не проинициализированными! Старая поговорка программистов гласит: "Не уверен — обнули".»

[Помни!]

Функция getData( ) возвращает адрес объекта.

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

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

[Советы]

►Списки в стандартной библиотеке...180

Точно так, как ребёнок должен научиться ходить перед тем как ездить на автомобиле, считать перед тем как использовать калькулятор, программисту необходимо научиться писать программы, работающие со связанными списками, перед тем как использовать классы списков, написанные другими. В главе 28, "Стандартная библиотека шаблонов", будут описаны классы-контейнеры, предоставляемые в распоряжение программиста средами С++, среди которых есть и класс, представляющий собой связанный список.

_________________

180 стр. Часть 3. Введение в классы

Глава 15. ЗАЩИЩЁННЫЕ ЧЛЕНЫ КЛАССА: НЕ БЕСПОКОИТЬ!...181

ОГЛАВЛЕНИЕ

В этой главе...

►Защищённые члены 181

►Чем хороши защищённые члены 183

►Обращение к защищённым членам 184

В главе 12, "Классы в С++", рассматривалась концепция классов. Ключевое слово public было описано как часть объявления класса, т.е. просто как то, что следует делать. В этой главе вы узнаете, что ключевому слову public есть альтернативы.

►Защищённые члены...181

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

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

[Помни!]

Зачем нужны защищённые члены...181

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

■■■

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

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

_________________

181 стр. Глава 15. Защищённые члены класса: не беспокоить!

■ Сокращение до минимума внешнего интерфейса класса. Гораздо проще изучать и использовать класс, который имеет ограниченный интерфейс ( а интерфейсом класса являются его открытые члены ). Защищённые члены скрыты от пользователя, и их не надо помнить ( в противном случае интерфейсом становится весь класс ). Такой подход называется абстракцией, которая описана в главе 11, "Знакомство с объектно-ориентированным программированием".

■ Уменьшение уровня взаимосвязи между классом и внешней программой. Ограничив взаимосвязь класса с внешним кодом, при необходимости гораздо проще заменить класс каким-либо другим.

■■■

Я так и слышу, как поклонники функционального подхода говорят: "Не нужно делать ничего противоестественного! Достаточно потребовать от программиста, чтобы он попросту не трогал некоторые члены класса".

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

Как устроены защищённые члены...182

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

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

addCourse ( int hours , float grade ) — добавить пройденный курс;

grade ( ) — вернуть текущую среднюю оценку;

hours ( ) — вернуть количество прослушанных часов.

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

class Student

{

public :

/* grade — возвращает текущую среднюю оценку */

float grade( )

{

return gpa ;

}

/* hours — возвращает количество прослушанных часов */

int hours( )

{

return semesterHours ;

}

/* addCourse — добавляет к записи студента прослушанный курс */

float addCourse( int hours , float grade )

/* Приведённые ниже члены недоступны для внешних функций */

protected :

int semesterHours ; /* Количество прослушанных часов */

float gpa ; /* Средняя оценка */

} ;

_________________

182 стр. Часть 3. Введение в классы

Теперь члены semesterHours и gpa доступны только из других членов класса Student, и приведённый ниже пример работать не будет.

Student s ;

int main( int argc , char* pArgs[ ] )

{

/* Повысим свой рейтинг ( но не слишком сильно, иначе никто не поверит ) */

s.gpa = 3.5 ; /* Вызовет ошибку при компиляции */

float gpa = s.grade( ) ; /* Эта открытая функция считывает значение переменной, но вы не можете непосредственно изменить её значение извне */

return 0 ;

}

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

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

[Советы]

«Члены класса могут быть защищены с помощью ещё одного ключевого слова — private. Кстати, по умолчанию при описании класса его члены считаются описанными именно как private. Разница между protected и private станет ясной при изучении наследования.»

[Технические подробности]

►Чем хороши защищённые члены...183

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

Защита внутреннего устройства класса...183

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

class Student

{

public :

/* grade — делает то же, что и раньше */

float grade( )

{

return gpa ;

}

/* Даём возможность изменения средней оценки */

float grade( float newGPA )

{

float oldGPA = gpa ;

/* Проверяем допустимость значения */

if ( newGPA > 0 && newGPA <= 4.0 )

{

gpa = newGPA ;

}

return oldGPA ;

}

/* ...всё остальное остаётся без изменений */

protected :

int semesterHours ; /* Количество прослушанных часов */

float gpa ;

} ;

_________________

183 стр. Глава 15. Защищённые члены класса: не беспокоить!

Добавление новой функции grade( float ) позволяет внешним приложениям изменять содержимое gpa. Заметьте, что класс всё равно не позволяет внешним функциям полностью контролировать содержимое своих защищённых членов. Внешнее приложение не может присвоить gpa любое значение, а только то, которое лежит в диапазоне между 0 и 4.0.

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

Классы с ограниченным интерфейсом...184

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

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

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

►Обращение к защищённым члена...184

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

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

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

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

_________________

184 стр. Часть 3. Введение в классы

class Student ;

{

friend void initialize( Student* ) ;

public :

/* Те же открытые члены, что и раньше */

protected :

int semesterHours ; /* Количество часов в семестре */

float gpa ;

} ;

/* Эта функция — друг класса Student и имеет доступ к его защищённым членам */

void initialize( Student *pS )

{

pS -> gpa =0 ; /* Теперь эти строки законны */

pS -> semesterHours = 0 ;

}

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

class Student ;

class Teacher

{

friend void registration( Teacher& , Student& ) ;

public :

void assignGrades( ) ;

protected :

int noStudents ;

Student *pList[ 100 ] ;

} ;

class Student

{

friend void registration( Teacher& , Student& ) ;

public :

/* Те же открытые члены, что и раньше */

protected :

Teacher *рТ ;

int semesterHours ; /* Количество часов в семестре */

float gpa ;

} ;

void registration( Teacher& , Student& )

{

/* Инициализация объекта Student */

s.semesterHours = 0 ;

s.gpa = 0 ;

/* Если есть место... */

if ( t.noStudents < 100 )

{

/* Добавляем в конец списка */

t.pList[ t.noStudents ] = &s ;

t.noStudents++ ;

}

}

_________________

185 стр. Глава 15. Защищённые члены класса: не беспокоить!

В данном примере функция registration( ) может обращаться к обоим классам — и Student и Teacher, связывая их на этапе регистрации, но при этом не входя в состав этих классов.

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

[Помни!]

Функция-член одного класса может быть объявлена как друг некоторого другого класса следующим образом:

class Teacher

{

/* Те же члены, что и раньше */

public :

void assignGrades( ) ;

} ;

class Student

{

friend void Teacher::assignGrades( ) ;

public :

/* Те же открытые члены, что и раньше */

protected :

/* Количество часов в семестре */

int semesterHours ;

float gpa ;

} ;

void Teacher::assignGrades( ) ;

{

/* Эта функция имеет доступ к  защищённым членам класса Student */

}

В отличие от примера с функциями — не членами, функция-член класса должна быть объявлена перед тем, как класс Student объявит её другом.

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

_________________

186 стр. Часть 3. Введение в классы

class Student ;

class Teacher

{

protected :

int noStudents ;

Student *pList [ 100 ] ;

public :

void assignGrades( ) ;

} ;

class Student

{

friend class Teacher ;

public :

/* Те же открытые члены, что и раньше */

protected :

Teacher *рТ ;

/* Количество часов в семестре */

int semesterHours ;

float gpa ;

} ;

Теперь любая функция-член класса Teacher имеет доступ ко всем защищённым членам класса Student. Объявление одного класса другом другого неразрывно связывает два класса.

_________________

187 стр. Глава 15. Защищённые члены класса: не беспокоить!

Глава 16. СОЗДАНИЕ И УДАЛЕНИЕ ОБЪЕКТОВ...188

ОГЛАВЛЕНИЕ

В этой главе...

►Создание объектов 188

►Использование конструкторов 189

►Что такое деструктор 194

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

►Создание объектов...188

Некоторые подчас теряются в терминах класс и объект. В чём разница между этими терминами? Как они связаны?

Я могу создать класс Dog, который будет описывать соответствующие свойства лучшего друга человека. К примеру, у меня есть две собаки. Это значит, что мой класс Dog содержит два экземпляра — Труди и Скутер ( надеюсь, что два: Скутера я не видел уже несколько дней... ).

«Класс описывает тип предмета, а объект — это экземпляр класса. Dog является классом, а Труди и Скутер — объектами. Каждая собака представляет собой отдельный объект, но существует только один класс Dog, при этом не имеет значения, сколько у меня собак.»

[Помни!]

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

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

«Глобальный объект является объектом, объявленным вне каких-либо функций. Локальный объект объявляется внутри функции, а следовательно, является локальным для функции. В приведённом ниже примере переменная me является глобальной, а переменная noМе — локальной по отношению к pickOne( ).»

[Помни!]

int me = 0 ;

void pickOne( )

{

int noMe ;

}

_________________

188 стр. Часть 3. Введение в классы

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

[Технические подробности]

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

►Использование конструкторов...189

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

Зачем нужны конструкторы...189

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

struct Student

{

int semesterHours ;

float gpa ;

} ;

void fn( )

{

Student s1 = { 0 , 0.0 } ;

// или

Student s2 ;

s2.semesterHours = 0 ;

s2.gpa = 0.0 ;

/* ...продолжение функции... */

}

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

class Student

{

public :

void init( )

{

semesterHours = 0 ;

gpa = 0.0 ;

}

/* ...прочие открытые члены... */

protected :

int semesterHours ;

float gpa ;

} ;

void fn( )

{

/* Создание объекта... */

Student s ;

/* ...и его инициализация */

s.init( ) ;

/* ...продолжение функции... */

}

_________________

189 стр. Глава 16. Создание и удаление объектов

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

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

Работа с конструкторами...190

Конструктор — это специальная функция-член, которая автоматически вызывается во время создания объекта. Конструктор должен иметь то же имя, что и класс. Таким образом компилятор сможет определить, что именно эта функция-член является конструктором. Конечно, создатели С++ могли сформулировать это правило как угодно, например, так: "Конструктором является функция с именем init( )". Как именно определено правило, не имеет значения; главное — чтобы конструктор мог быть распознан компилятором. Ещё одним свойством конструктора является то, что он не возвращает никакого значения, поскольку вызывается автоматически ( если бы конструктор и возвращал значение, его всё равно некуда было бы записать ).

Конструирование одного объекта...190

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

//

/* Constructor — пример вызова конструктора */

//

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

class Student

{

public :

Student( )

{

cout << "Конструируем Student" << endl ;

semesterHours = 0 ;

gpa = 0.0 ;

}

/* ...прочие открытые члены... */

protected :

int semesterHours ;

float gpa ;

} ;

int main ( int nNumberofArgs , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

cout << "Создание нового объекта Student" << endl ;

Student s ;

cout << "Создание нового объекта Student в куче" << endl ;

Student* pS = new Student ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

_________________

190 стр. Часть 3. Введение в классы

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

Создание нового объекта Student

Конструируем Student

Создание нового объекта Student в куче

Конструируем Student

Press any key to continue...

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

class Student

{

public :

Student( ) ;

/* ...Остальные открытые члены... */

protected :

int semesterHours ;

float gpa ;

} ;

Student::Student( )

{

cout << "Конструируем Student\n" ;

semesterHours = 0 ;

gpa = 0.0 ;

}

«В данном примере добавлена небольшая функция main( ), чтобы эту тестовую программу можно было запустить. Настоятельно рекомендую пройти эту программу в пошаговом режиме отладчика перед тем, как двигаться дальше. О том, как это сделать, вы можете прочесть в главе 10 , "Отладка программ на С++".»

[Советы]

Выполняя этот пример в пошаговом режиме, дойдите до строки с объявлением объекта s. Выполните команду отладчика Шаг внутрь ( Step into ), и управление как по волшебству перейдёт к функции Student::Student( ). Продолжайте выполнение конструктора в пошаговом режиме. Когда функция закончится, управление перейдёт к следующей за объявлением объекта класса строке.

«В некоторых случаях команда Шаг внутрь ( Step into ) выполняет весь конструктор сразу, за один шаг. В таком случае вы можете просто установить в нём точку останова, что сработает в любом случае.»

[Атас!]

_________________

191 стр. Глава 16. Создание и удаление объектов

Конструирование нескольких объектов...192

Каждый элемент массива конструируется отдельно. Внесём в программу Constructor небольшие изменения.

//

/* ConstructArray — пример вызова конструкторов */

/*                          для массива объектов */

//

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

class Student

{

public :

Student( )

{

cout << "Конструируем Student" << endl ;

semesterHours = 0 ;

gpa = 0.0 ;

}

/* ...прочие открытые члены... */

protected :

int semesterHours ;

float gpa ;

} ;

int main( int nNumberofArgs , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

cout << "Создание массива из 5 объектов Student" << endl ;

Student s[ 5 ] ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

Вывод этой программы выглядит следующим образом:

Создание массива из 5 объектов Student

Конструируем Student

Конструируем Student

Конструируем Student

Конструируем Student

Конструируем Student

Press any key to continue...

Конструирование составных объектов...192

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

_________________

192 стр. Часть 3. Введение в классы

//

/* ConstructMembers — объекты-члены класса */

/*                 конструируются до конструирования */

/*                 класса, содержащего эти объекты */

//

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

class Course

{

public :

Course( )  /* пятый ход */

{

cout << "Конструируем Course" << endl ;

}

} ;

class Student

{

public :

Student( ) /* второй ход */

{

cout << "Конструируем Student" << endl ;

semesterHours = 0 ;

gpa = 0.0 ;

}

protected :

int semesterHours ;

float gpa ;

} ;

class Teacher

{

public :

Teacher( )  /* шестой ход */

{

cout << "Конструируем Teacher" << endl ;

}

protected :

Course c ;  /* четвёрый ход */

} ;

class TutorPair

{

public :

TutorPair( ) /* седьмой ход */

{

cout << "Конструируем TutorPair" << endl ;

noMeetings = 0 ;

}

protected :

Student student ; /* первый ход*/

Teacher teacher ; /* третий ход */

int noMeetings ;

} ;

int main( int nNumberofArgs , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

cout << "Создаём объект TutorPair" << endl ;

TutorPair tp ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

_________________

193 стр. Глава 16. Создание и удаление объектов

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

Создаём объект TutorPair

Конструируем Student

Конструируем Course

Конструируем Teacher

Конструируем TutorPair

Press any key to continue...

Создание объекта tp в main( ) автоматически вызывает конструктор TutorPair. Перед тем как управление будет передано телу конструктора TutorPair, вызываются конструкторы для объектов-членов student и teacher.

Конструктор Student вызывается первым, поскольку объект этого класса объявлен первым. Затем вызывается конструктор Teacher.

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

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

«Это не означает, что TutorPair отвечает за инициализацию Student и Teacher. Каждый класс отвечает за инициализацию своего объекта, где бы тот ни создавался.»

[Помни!]

►Что такое деструктор...194

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

Зачем нужен деструктор...194

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

Работа с деструкторами...194

Деструктор имеет то же имя, что и класс, но только с предшествующим ему символом тильды ( ~ ) ( С++ последователен и здесь: ведь символ тильды не что иное, как символ оператора "нет", т.е. деструктор — это отрицание конструктора ).

_________________

194 стр. Часть 3. Введение в классы

Как и конструктор, деструктор не имеет типа возвращаемого значения. С учётом сказанного деструктор класса Student будет выглядеть так:

class Student

{

public :

Student( )

{

semesterHours = 0 ;

gpa = 0.0 ;

}

~Student( )

{

/* Все используемые ресурсы освобождаются здесь */

}

/* ...остальные открытые члены... */

protected :

int semesterHours ;

float gpa ;

} ;

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

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

//

/* DestructMembers — демонстрация использования */

/*                  конструкторов и деструкторов */

//

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

class Course

{

public :

Course( )    { cout << "Конструктор Course" << endl ; }

~Course( ) { cout << "Деструктор Course" << endl ; }

} ;

class Student

{

public :

Student( )

{

cout << "Конструктор Student" << endl ;

semesterHours = 0 ;

gpa = 0.0 ;

_________________

195 стр. Глава 16. Создание и удаление объектов

}

~Student( ) { cout << "Деструктор Student" << endl ; }

protected :

int semesterHours ;

float gpa ;

} ;

class Teacher

{

public :

Teacher( )

{

cout << "Конструктор Teacher" << endl ;

pC = new Course ;

}

~Teacher( )

{

cout << "Деструктор Teacher" << endl ;

delete pC ;

}

protected :

Course* pC ;

} ;

class TutorPair

{

public :

TutorPair( )

{

cout << "Конструктор TutorPair" << endl ;

noMeetings = 0 ;

}

~TutorPair( ) { cout << "Деструктор TutorPair" << endl ; }

protected :

Student student ;

Teacher teacher ;

int noMeetings ;

} ;

TutorPair* fn( )

{

cout << "Создание объекта TutorPair в функции fn( )"

<< endl ;

TutorPair tp ;

cout << "Создание объекта TutorPair в куче" << endl ;

TutorPair* pTP = new TutorPair ;

cout << "Возврат из функции fn ( )" << endl ;

return pTP ;

}

int main( int nNumberofArgs , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

/* Вызов функции fn( ) и возврат объекта TutorPair в куче */

TutorPair* pTPReturned = fn( ) ;

cout << "Получен объект в куче" << endl ;

delete pTPReturned ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

_________________

196 стр. Часть 3. Введение в классы

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

При выполнении программы вы увидите на экране следующее.

Создание объекта TutorPair в функции fn( )

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

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

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

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

Создание объекта TutorPair в куче

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

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

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

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

Возврат из функции fn( )

Деструктор TutorPair

Деструктор Teacher

Деструктор Course

Деструктор Student

Получен объект в куче

Деструктор TutorPair

Деструктор Teacher

Деструктор Course

Деструктор Student

Press any key to continue...

Здесь создаются два объекта TutorPair. Первый, tp, является локальным объектом функции fn( ), а второй, рТР, размещается в куче. Первый объект выходит из области видимости при возврате из функции и уничтожается автоматически, а второй остаётся до тех пор, пока функция main( ) не уничтожает его явным образом.

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

[Помни!]

_________________

197 стр. Глава 16. Создание и удаление объектов

Глава 17. АРГУМЕНТАЦИЯ КОНСТРУИРОВАНИЯ...198

ОГЛАВЛЕНИЕ

В этой главе...

►Как снабдить конструктор аргументами 198

►Перегрузка конструктора 200

►Определение конструкторов по умолчанию 203

►Конструирование членов класса 204

►Управление последовательностью конструирования 208

Класс представляет тип объекта в реальном мире. Например, мы использовали класс Student для представления студента и его свойств. Точно так же, как и студенты, классы считают себя абсолютно самостоятельными. Однако, в отличие от студентов, класс действительно сам "ухаживает" за собой — он должен всё время поддерживать себя в приемлемом состоянии.

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

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

►Как снабдить конструктор аргументами...198

С++ позволяет программисту определить конструктор с аргументами, например:

class Student

{

public :

Student( char *pName ) ;

/* Продолжение класса Student */

} ;

Зачем конструкторам нужны аргументы...198

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

_________________

198 стр. Часть 3. Введение в классы

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

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

Как использовать конструктор с аргументами...199

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

При этом нельзя забывать, что вы вызываете конструктор не как нормальную функцию и передать ему аргумент можно только в момент создания объекта. Так, приведённая ниже программа создаёт объект s класса Student, вызывая конструктор Student( char* ). Объект s уничтожается в момент возврата из функции main( ).

//

/* ConstructorWArg — конструктор с аргументами */

//

#include <cstdio>

#include <cstdlib>

#include <iostream>

#include <strings.h>

using namespace std ;

const int MAXNAMESIZE = 40 ;

class Student

{

public :

Student( char* pName )

{

strncpy( name , pName , MAXNAMESIZE ) ;

name[ MAXNAMESIZE - 1 ] = '\0' ;

semesterHours = 0 ;

gpa = 0.0 ;

}

/* ...прочие открытые члены... */

protected :

char name[ MAXNAMESIZE ] ;

int semesterHours ;

float gpa ;

} ;

int main( int argcs , char* pArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

Student s( "О. Danny Boy" ) ;

Student* pS = new Student( "E. Z. Rider" ) ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

_________________

199 стр. Глава 17. Аргументация конструирования

В этом примере конструктор выглядит почти так же, как и конструктор из главы 16, "Создание и удаление объектов", с тем лишь отличием, что он принимает аргумент pName, имеющий тип char*. Этот конструктор инициализирует все данные-члены нулевыми значениями, за исключением члена name, который инициализируется строкой pName.

Объект s создаётся в функции main( ). Аргумент, передаваемый конструктору, находится в строке объявления s сразу же за именем объекта. Благодаря такому объявлению студент s получил имя Danny. Закрывающая фигурная скобка функции main( ) вызывает гром и молнию деструктора на голову несчастного Danny. Аналогично создаётся объект в куче.

«Многие конструкторы в этой главе нарушают правило "функции размером больше трёх строк не должны быть inline-функциями". Я просто решил облегчить вам чтение ( а теперь — ваши аплодисменты! ).»

[Атас!]

►Перегрузка конструктора...200

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

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

[Помни!]

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

/* OverloadConstructor — несколько способов */

/*                          создать объект путём */

/*                    перегрузки конструктора */

#include <cstdio>

#include <cstdlib>

#include <iostream>

#include <strings.h>

using namespace std ;

const int MAXNAMESIZE = 40 ;

class Student

{

public :

Student( )

_________________

200 стр. Часть 3. Введение в классы

{

cout << "Конструктор Student( )" << endl ;

semesterHours = 0 ;

gpa = 0.0 ;

name[ 0 ] = '\0' ;

}

Student( char *pName )

{

cout << "Конструктор Student( " << pName

<<" )" << endl ;

strncpy( name , pName , MAXNAMESIZE ) ;

name[ MAXNAMESIZE - 1 ] = '\0' ;

semesterHours = 0 ;

gpa = 0 ;

}

Student( char *pName , int xfrHours , float xfrGPA )

{

cout << "Конструктор Student( " << pName << ","

<< xfrHours << "," << xfrGPA << " )" << endl ;

strncpy( name , pName , MAXNAMESIZE ) ;

name[ MAXNAMESIZE - 1 ] = '\0' ;

semesterHours = xfrHours ;

gpa = xfrGPA ;

}

~Student( )

{

cout << "Деструктор Student" << endl ;

}

protected :

char name[ 40 ] ;

int semesterHours ;

float gpa ;

} ;

int main( int argcs , char* pArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

/* Вызов трёх разных конструкторов */

Student noName ;

Student freshman( "Marian Haste" ) ;

Student xferStudent( "Pikumup Andropov" , 80 , 2.5 ) ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

Поскольку объект noName реализован без аргументов, он конструируется с использованием Student::Student( ), который называется конструктором по умолчанию или пустым конструктором. ( Я предпочитаю последнее название, но, поскольку первое более распространённое, в этой книге будет использоваться именно оно. ) Объект freshMan создаётся с использованием конструктора, которому нужен только один аргумент типа char*; объекту xfеr требуется конструктор с тремя аргументами.

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

_________________

201 стр. Глава 17. Аргументация конструирования

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

/* ConstructorWDefaults — несколько конструкторов */

/*                        зачастую могут быть */

/*                        бъединены в один */

#include <cstdio>

#include <cstdlib>

#include <iostream>

#include <strings.h>

using namespace std ;

const int MAXNAMESIZE = 40 ;

class Student

{

public :

Student( char *pName = "no name" ,

int xfrHours = 0 ,

float xfrGPA = 0.0 )

{

cout << "Конструктор Student( " << pName<< ","

<< xfrHours << "," << xfrGPA << " )" << endl ;

strncpy( name , pName , MAXNAMESIZE ) ;

name[ MAXNAMESIZE - 1 ] = '\0' ;

semesterHours = xfrHours ;

gpa = xfrGPA ;

}

~Student( )

{

cout << "Деструктор Student" << endl ;

}

/* ...прочие открытые члены... */

protected :

char name[ MAXNAMESIZE ] ;

int semesterHours ;

float gpa ;

} ;

int main( int argcs , char* pArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

/* Вызов одного и того же конструктора */

Student noName ;

Student freshman( "Marian Haste" ) ;

Student xferStudent( "Pikumup Andropov" , 80 , 2.5 ) ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

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

_________________

202 стр. Часть 3. Введение в классы

«В ранних версиях С++ вы не смогли бы создать конструктор по умолчанию, предусмотрев значения по умолчанию для всех аргументов. Конструктор по умолчанию должен был быть определён явно. Так что будьте готовы к тому, что некоторые старые версии компиляторов могут потребовать явного определения конструктора по умолчанию.»

[Помни!]

►Определение конструкторов по умолчанию...203

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

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

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

[Атас!]

Приведённый ниже фрагмент демонстрирует сказанное. Этот пример вполне корректен.

class Student

{

/* ...то же, что и раньше, только без конструкторов */

} ;

int main( int argcs , char* pArgs[ ] )

{

Student noName ;

return 0 ;

}

Приведённый далее пример компилятор с негодованием отвергнет.

class Student

{

public :

Student( char *pName ) ;

} ;

int main( int argcs , char* pArgs[ ] )

{

Student noName ;

return 0 ;

}

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

_________________

203 стр. Глава 17. Аргументация конструирования

Не попадитесь в ловушку

♦♦♦♦♦

Ещё раз взгляните на объявление объектов класса student из приведённого выше примера:

Student noName ;

Student freshMan( "Smell E. Fish" ) ;

Student xfer( "Upp R. Classman" , 80 , 2.5 ) ;

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

Student noName( ) ;

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

Student thisIsAFunc( int ) ;

Student thisIsAnObject( 10 ) ;

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

♦♦♦♦♦

►Конструирование членов класса...204

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

/* ConstructingMembers — передача параметров */

/*                      конструктору члена */

#include <cstdio>

#include <cstdlib>

#include <iostream>

#include <strings.h>

using namespace std ;

const int MAXNAMESIZE = 40 ;

int nextStudentId = 0 ;

class StudentId

{

public :

StudentId( )

{

value = ++nextStudentId ;

cout << "Присвоение id " << value << endl ;

}

protected :

_________________

204 стр. Часть 3. Введение в классы

int value ;

} ;

class Student

{

public :

Student( char* pName )

{

cout << "Конструктор Student( " << pName

<< " )" << endl ;

strncpy( name , pName , MAXNAMESIZE ) ;

name[ MAXNAMESIZE - 1 ] = '\0' ;

semesterHours = 0 ;

gpa = 0.0 ;

}

/* ...прочие открытые члены... */

protected :

char name[ MAXNAMESIZE ] ;

int semesterHours ;

float gpa ;

StudentId id ;

} ;

int main( int argcs , char* pArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

Student s( "Chester" ) ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

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

Наш класс Student содержит член id, который является экземпляром класса StudentId. Конструктор класса Student не может присвоить значение члену id, поскольку Student не имеет доступа к защищённым членам класса StudentId. Можно было бы сделать Student другом класса StudentId, но такой подход нарушил бы положение объектно-ориентированного программирования, утверждающее, что каждый класс должен заниматься своим делом. Нам нужна возможность вызывать конструктор класса StudentId в процессе создания класса Student.

С++ делает это автоматически, инициализируя член id с помощью конструктора по умолчанию StudentId::StudentId( ). Это происходит после вызова конструктора класса Student, но до того, как управление передаётся первой строке этого конструктора. ( Выполните в пошаговом режиме приведённую выше программу, и вы поймёте, о чём я говорю. ) Выполнение приведённой выше программы выведет на экран следующие строки:

Присвоение id 1

Конструктор Student( Chester )

Press any key to continue...

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

_________________

205 стр. Глава 17. Аргументация конструирования

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

Теперь мы знаем, что будет с конструктором по умолчанию. Но что, если мы захотим вызвать другой конструктор? Куда в этом случае нужно поместить объект? Я имею в виду следующее: представьте себе, что вместо автоматической генерации идентификатора студента, необходимо передать его конструктору Student, который в свою очередь, должен передать его конструктору StudentId.

«Для начала я покажу вам способ, который работать не будет. ( Здесь приведена только существенная для понимания часть кода — полностью программа ConstructSeparateID.cpp находится на прилагаемом компакт-диске . )»

[Диск]

class Student

{

public :

Student( char *pName = "no name" , int ssId = 0 )

{

cout << "Конструктор Student( " << pName

<< " )" << endl ;

strncpy( name , pName , MAXNAMESIZE ) ;

name[ MAXNAMESIZE - 1 ] = '\0' ;

semesterHours = 0 ;

gpa = 0.0 ;

/* Вот это можно и не пытаться делать - толку не будет */

StudentId id( ssId ) ;

}

protected :

char name[ MAXNAMESIZE ] ;

StudentId id ;

} ;

Конструктор класса StudentId был переписан так, чтобы он мог принимать внешнее значение ( значение по умолчанию необходимо для того, чтобы приведённый фрагмент откомпилировался без ошибок, которые появятся в противном случае; почему — станет понятно чуть позже ). Внутри конструктора Student программист ( т.е. я ) попытался невиданным доселе способом сконструировать объект id класса StudentId.

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

Присвоение id 0

Конструктор Student( Chester )

Присвоение id 1234

Деструктор id 1234

Сообщение из функции main( )

Press any key to continue...

Деструктор id 0

Первая проблема заключается в том, что конструктор класса StudentId вызывается дважды: сначала с нулём и только затем с ожидаемым числом 1234. Кроме того, объект с идентификатором 1234 ликвидируется перед выводом сообщения от main( ). Очевидно, объект класса StudentId ликвидируется внутри конструктора класса Student.

_________________

206 стр. Часть 3. Введение в классы

Объяснить такое странное поведение программы довольно просто. Член id уже существует к моменту перехода управления к телу конструктора Student. Поэтому вместо инициализации уже существующего члена id объявление в последней строке конструктора Student вызывает создание локального объекта с таким же именем. Этот локальный объект и уничтожается при выходе из конструктора.

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

[Диск]

//

/* ConstructDataMember — передача параметра */

/*                      конструктору члена */

//

#include <cstdio>

#include <cstdlib>

#include <iostream>

#include <strings.h>

using namespace std ;

const int MAXNAMESIZE = 40 ;

class StudentId

{

public :

StudentId( int id = 0 )

{

value = id ;

cout << "Присвоение id " << value << endl ;

}

~StudentId( )

{

cout << "Деструктор id " << value << endl ;

}

protected :

int value ;

} ;

class Student

{

public :

Student( char *pName = "no name" , int ssId = 0 )

: id( ssId )

{

cout << "Конструктор Student( " << pName

<< " )" << endl ;

strncpy( name , pName , MAXNAMESIZE ) ;

name[ MAXNAMESIZE - 1 ] - '\0' ;

}

protected :

char name[ 40 ] ;

StudentId id ;

} ;

int main( int argcs , char* pArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

Student s( "Chester" , 1234 ) ;

cout << "Сообщение из функции main" << endl ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

_________________

207 стр. Глава 17. Аргументация конструирования

Обратите особое внимание на первую строку конструктора. В этой строке есть кое-что, с чем вы до этого не встречались. Следующая за двоеточием команда вызывает конструкторы членов данного класса. Компилятор С++ прочтёт эту строку так: "Вызвать конструктор для члена id с аргументом ssId. Все остальные данные-члены, не вызванные явно, строить с использованием конструктора по умолчанию".

Результат работы этой программы свидетельствует, что всё получилось так, как мы и хотели.

Присвоение id 1234

Конструктор Student( Chester )

Сообщение из функции main

Press any key to continue...

Деструктор id 1234

Конструкторы константных членов...208

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

class Mammal

{

public :

Mammal( int nof ) : numberOfFeet( nof )

{ }

protected :

const int numberOfFeet ;

} ;

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

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

[Советы]

►Управление последовательностью конструирования...208

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

_________________

208 стр. Часть 3. Введение в классы

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

■■■

■ Локальные и статические объекты создаются в том порядке, в котором они объявлены в программе.

■ Статические объекты создаются только один раз.

■ Все глобальные объекты создаются до вызова функции main( ).

■ Нет какого-либо определённого порядка создания глобальных объектов.

■ Члены создаются в том порядке, в котором они объявлены внутри класса.

■ Деструкторы вызываются в порядке, обратном порядку вызова конструкторов.

■■■

«Статическая переменная — это переменная, которая является локальной по отношению к функции, но при этом сохраняет своё значение между вызовами функции. Глобальная переменная — это переменная, объявленная вне какой-либо функции.»

[Помни!]

Рассмотрим каждое из приведённых выше правил.

Локальные объекты создаются последовательно...209

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

Статические объекты создаются один раз...209

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

/* ConstructStatic — демонстрация однократного */

/*                создания статических объектов */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

class DoNothing

{

public :

DoNothing( int initial )

{

cout << "DoNothing сконструирован со значением "

<< initial

<< endl ;

}

} ;

_________________

209 стр. Глава 17. Аргументация конструирования

void fn( int i )

{

cout << "Функции fn передано значение " << i << endl ;

static DoNothing dn( i ) ;

}

int main( int argcs , char* pArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

fn( 10 ) ;

fn( 20 ) ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

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

Функции fn передано значение 10

DoNothing сконструирован со значением 10

Функции fn передано значение 20

Press any key to continue...

Обратите внимание, что сообщение от функции fn( ) появилось дважды, а сообщение от конструктора DoNothing — только при первом вызове fn( ).

Все глобальные объекты создаются до вызова main( )...210

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

«При отладке такой порядок может привести к неприятностям. Некоторые отладчики пытаются выполнить весь код, который находится до main( ), и только потом передать управление пользователю. Это прекрасно подходит для С, поскольку до входа в функцию main( ) там не может быть никакого кода, написанного пользователем. Однако в С++ это может стать причиной большой головной боли, поскольку тела конструкторов для всех глобальных объектов к моменту передачи управления main( ) уже выполнены. Если хоть один из этих конструкторов содержит серьёзный "жучок", программа погибнет до того, как начнёт выполняться!»

[Атас!]

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

Порядок создания глобальных объектов не определён...210

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

_________________

210 стр. Часть 3. Введение в классы

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

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

class Student

{

public :

Student ( unsigned id ) : studentId( id ) { }

const int StudentId ;

} ;

class Tutor

{

public :

Tutor ( Student& s ) : tutoredId( s.studentId ) { }

int tutoredId ;

} ;

/* Создаём студента */

Student randy( 1234 ) ;

/* Назначаем студенту учителя */

Tutor jenny( randy ) ;

В этом примере конструктор Student присваивает студенту идентификатор, а конструктор класса Tutor записывает этот идентификатор студента, которому нужен учитель. Программа объявляет студента randy, а затем назначает ему учителя jenny.

При этом подразумевается, что randy создаётся раньше, чем jenny; в этом-то и состоит проблема. Представьте себе, что порядок создания этих объектов будет другим. Тогда объект jenny будет построен с использованием блока памяти, который пока что не является объектом типа Student, а значит, вместо идентификатора студента в randy будет находиться непредсказуемое значение.

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

[Советы]

Члены создаются в порядке их объявления...211

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

class Student

{

public :

Student ( int id , int age ) : sAge( age ) , sId( id ) { }

const int sId ;

const int sAge ;

} ;

_________________

211 стр. Глава 17. Аргументация конструирования

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

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

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

_________________

212 стр. Часть 3. Введение в классы

Глава 18. КОПИРУЮЩИЙ КОНСТРУКТОР...213

ОГЛАВЛЕНИЕ

В этой главе...

►Копирование объекта 213

►Автоматический конструктор копирования 215

►"Мелкие" и "глубокие" копии 217

►Временные объекты 221

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

►Копирование объекта...213

Конструктор, который используется С++ для создания копий объекта, называется копирующим конструктором, или конструктором копирования. Он имеет вид X::Х( Х& ) ( или X::X( const Х& ) ), где X — имя класса. Да, это не ошибка — это действительно конструктор класса X, который требует в качестве аргумента ссылку на объект класса X. Это звучит несколько бессмысленно, но не торопитесь с выводами и позвольте объяснить, зачем такое "чудо" в С++.

Зачем нужен копирующий конструктор...213

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

void fn( Student fs )

{

/* Некоторые действия */

}

int main( int argcs , char* pArgs[ ] )

{

Student ms ;

fn( ms ) ;

return 0 ;

}

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

_________________

213 стр. Глава 18. Копирующий конструктор

«В С++ аргументы функции передаются по значению.»

[Помни!]

Теперь попробуем понять, что же значит — создать копию объекта. Для этого требуется конструктор, который будет создавать объект ( даже если копируется уже существующий объект ). С++ мог бы побайтово скопировать существующий объект в новый, но как быть, если побайтовая копия не совсем то, что нам нужно? Что, если мы хотим нечто иное? ( Не спрашивайте у меня пока, что такое это "иное" и зачем оно нужно. Немного терпения! ) У нас должна быть возможность самим определять, как будет создаваться копия объекта.

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

Использование конструктора копирования ...214

Лучший путь понять, как работает конструктор копирования, — это увидеть его в действии. Рассмотрим приведённый ниже пример с классом Student.

//

/* CopyConstructor — работа конструктора копирования */

//

#include <cstdio>

#include <cstdlib>

#include <iostream>

#include <strings.h>

using namespace std ;

const int MAXNAMESIZE = 40 ;

class Student

{

public :

/* conventional constructor — обычный конструктор */

Student( char *pName = "no name" , int ssId = 0 )

{

strcpy( name , pName ) ;

id = ssId ;

cout << "Конструируем " << name << endl ;

}

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

Student( Student& s )

{

strcpy( name , "Копия " ) ;

strcat( name , s.name ) ;

id = s.id ;

cout << "Сконструирована " << name << endl ;

}

~Student( )

{

cout << "Деструкция " << name << endl ;

}

protected :

_________________

214 стр. Часть 3. Введение в классы

char name[ MAXNAMESIZE ] ;

int id ;

} ;

/* fn — передача параметра по значению */

void fn( Student copy )

{

cout << "В функции fn( )" << endl ;

}

int main( int nNumberofArgs , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

Student Chester( "Chester" , 1234 ) ;

cout << "Вызов fn( )" << endl ;

fn( Chester ) ;

cout << "Возврат из fn( )" << endl ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

После запуска этой программы на экран будут выведены следующие строки:

Конструируем Chester

Вызов fn( )

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

В функции fn( )

Деструкция Копия Chester

Возврат из fn( )

Press any key to continue...

Деструкция Chester

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

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

►Автоматический конструктор копирования...215

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

_________________

215 стр. Глава 18. Копирующий конструктор

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

[Диск]

/* DefaultCopyConstructor — демонстрация вызова */

/*                    конструктором копирования по */

/*                     умолчанию конструкторов */

/*                     копирования членов */

#include <cstdio>

#include <cstdlib>

#include <iostream>

#include <strings.h>

using namespace std ;

const int MAXNAMESIZE = 40 ;

class Student

{

public :

Student( char *pName = "no name" )

{

strcpy( name , pName ) ;

cout << "Конструируем " << name << endl ;

}

Student( Student& s )

{

strcpy( name , "Копия " ) ;

strcat( name , s.name ) ;

cout << "Сконструирована " << name << endl ;

}

~Student( )

{

cout << "Деструкция " << name << endl ;

}

protected :

char name[ MAXNAMESIZE ] ;

} ;

class Tutor

{

public :

/* Вызов конструктора копирования Student */

Tutor( Student& s ) : student( s )

{

cout << "Конструирование объекта Tutor" << endl ;

id = 0 ;

}

protected :

_________________

216 стр. Часть 3. Введение в классы

Student student ;

int id ;

} ;

void fn( Tutor tutor )

{

cout << "В функции fn( )" << endl ;

}

int main( int argcs , char* pArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

Student Chester( "Chester" ) ;

Tutor tutor( Chester ) ;

cout << "Вызов fn ( )" << endl ;

fn( tutor ) ;

cout << "Возврат из fn( )" << endl ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

Запуск этой программы приведёт к выводу таких сообщений:

Конструируем Chester

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

Конструирование объекта Tutor

Вызов fn( )

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

В функции fn( )

Деструкция Копия Копия Chester

Возврат из fn( )

Press any key to continue . . .

Деструкция Копия Chester

Деструкция Chester

Конструирование объекта Chester приводит к вызову конструктора Student, который выводит первое сообщение. Конструктор объекта tutor вызывает копирующий конструктор Student для генерации собственного члена student, и приводит к выводу следующих двух строк.

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

Конструктор копирования по умолчанию Tutor вызывает конструкторы копирования для членов-данных. Копирующий конструктор для int просто копирует значение, но конструктор копирования Student генерирует вывод на экран строки Сконструирована Копия Копия Chester. Деструктор копии вызывается как часть возврата из функции fn( ).

►"Мелкие " и "глубокие " копии...217

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

_________________

217 стр. Глава 18. Копирующий конструктор

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

/* ShallowCopy — мелкое копирование */

/*              неприменимо при захвате */

/*                ресурсов */

//

#include <cstdio>

#include <cstdlib>

#include <iostream>

#include <string.h>

using namespace std ;

class Person

{

public :

Person( char *pN )

{

cout << "Конструирование \" " << pN << " \" " << endl ;

pName = new char[ strlen( pN ) + 1 ] ;

if ( pName != 0 )

{

strcpy( pName , pN ) ;

}

}

~Person( )

{

cout << "Деструкция \" " << pName << " \" " << endl ;

strcpy( pName , "Уже освобождённая память" ) ;

/* delete pName ; */

}

protected :

char *pName ;

} ;

void fn( )

{

/* Создание нового объекта */

Person p1( "Достаточно длинное имя" ) ;

/* Копирование p1 в р2 */

Person p2(p1);

}

int main( int argcs , char* pArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

cout << "Вызов fn( )" << endl ;

fn( ) ;

cout << "Возврат из fn( )" << endl ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

_________________

218 стр. Часть 3. Введение в классы

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

Вызов fn( )

Конструирование "Достаточно длинное имя"

Деструкция "Достаточно длинное имя"

Деструкция "Уже освобождённая память"

Возврат из fn( )

Press any key to continue...

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

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

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

[Атас!]

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

Когда объекты ликвидируются, деструктор для р2 первым получает доступ к этому блоку памяти. Этот деструктор стирает имя и освобождает блок памяти. К тому времени как деструктор p1 получает доступ к этому блоку, память уже очищена, а имя стёрто. Теперь понятно, откуда взялось сообщение об ошибке. Суть проблемы проиллюстрирована на рис. 18.1. Объект p1 копируется в новый объект р2, но не копируются используемые им ресурсы. Таким образом, р1 и р2 указывают на один и тот же ресурс ( в данном случае это блок памяти ). Такое явление называется "мелким" ( shallow ) копированием, поскольку при этом копируются только члены класса как таковые.

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

[Диск]

class Person

{

public :

Person( char *pN )

{

cout << "Конструирование " << pN << endl ;

pName = new char[ strlen( pN ) + 1 ] ;

if ( pName != 0 )

{

strcpy( pName , pN ) ;

}

}

_________________

219 стр. Глава 18. Копирующий конструктор

/* Копирующий конструктор выделяет новый блок памяти из кучи */

Person( Person& p )

{

cout << "Копирование " << p.pName

<< " в собственный блок" << endl ;

pName = new char[ strlen( p.pName ) + 1 ] ;

if ( pName != 0 )

{

strcpy( pName , p.pName ) ;

}

}

~Person( )

{

cout << "Деструкция " << pName << endl ;

strcpy( pName , "Уже освобождённая память" ) ;

/* delete pName ; */

}

protected :

char *pName ;

} ;

Рис. 18.1. Мелкое копирование объекта p1 в р2

Здесь копирующий конструктор выделяет новый блок памяти для имени, а затем копирует содержимое блока памяти исходного объекта в этот новый блок ( рис. 18.2 ). Такое копирование называется "глубоким" ( deep ), поскольку копирует не только элементы, но и занятые ими ресурсы ( конечно, аналогия, как говорится, притянута за уши, но ничего не поделаешь — не я придумал эти термины ).

Запуск программы с новым копирующим конструктором приведёт к выводу на экран следующих строк:

Вызов fn( )

Конструирование Достаточно_длинное_имя

Копирование Достаточно_длинное_имя в собственный блок

Деструкция Достаточно_длинное_имя

Деструкция Достаточно_длинное_имя

Возврат из fn( )

Press any key to continue...

_________________

220 стр. Часть 3. Введение в классы

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

Рис. 18.2. Глубокое копирование объекта p1 в р2

►Временные объекты...221

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

Student fn( ) ; /* Возвращает объект по значению */

int main ( int argcs , char* pArgs[ ] )

{

Student s ;

s = fn( ) ; /* В результате вызова fn( ) будет создан временный объект */

return 0 ;

}

Функция fn( ) возвращает объект по значению. В конечном счёте этот объект будет скопирован в s, но где он находится до этого?

Для хранения таких объектов С++ создаёт временные объекты ( такие объекты создаются и в некоторых других случаях ). "Хорошо, — скажете вы, — С++ создаёт временные объекты, но откуда он знает, когда их надо уничтожать?" ( Спасибо за хороший вопрос! ) В нашем примере это не имеет особого значения, поскольку временный объект выйдет из области видимости, как только копирующий конструктор скопирует его в s. Но что, если s будет определено как ссылка?

_________________

221 стр. Глава 18. Копирующий конструктор

int main ( int argcs , char* pArgs[ ] )

{

Student& refS = fn( ) ;

/* ...Что теперь?... */

return 0 ;

}

Теперь период жизни временного объекта имеет большое значение, поскольку ссылка refS продолжает своё существование независимо от существования объекта! В приведённом ниже примере я отметил место, начиная с которого временный объект становится недоступен.

Student fn1( ) ;

int fn2( Student& ) ;

int main ( int argcs , char* pArgs[ ] )

{

int x ;

/* Создаём объект Student, вызывая fn1( ), а затем передаём этот объект функции fn2( ) . fn2( ) возвращает целочисленное значение, которое используется для выполнения некоторых вычислений. Весь этот период временный объект, возвращённый функцией fn1( ), доступен */

х = 3*fn2( fn1( ) ) + 10 ;

/* Временный объект, который вернула функция fn1( ), становится недоступен */

/* ...Остальной код... */

return 0 ;

}

Таким образом, пример с использованием ссылки неверен, поскольку объект выйдет из области видимости, a refS будет продолжать существовать, и в результате ссылка будет указывать на несуществующий объект.

Как избегать временных объектов...222

Вы можете подумать, что изучение всего этого копирования объектов туда и обратно — пустая трата времени. Что, если вы не хотите делать все эти копии? Самое простое решение заключается в передаче и приёме объектов функции по ссылке. Это исключает все описанные неприятности.

Но как убедиться, что С++ не создаёт временных объектов незаметно для вас? Допустим, ваш класс использует ресурсы, которые вы не хотите копировать. Что же вам делать?

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

class Student

{

protected :

Student( Student& s ){ }

public :

/* ...Всё остальное как обычно... */

} ;

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

_________________

222 стр. Часть 3. Введение в классы

Аргумент копирующего конструктора...223

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

class Student

{

public :

Student( Student s )

{

/* ...всё, что угодно... */

}

} ;

void fn( Student fs )

{

}

int main ( int argcs , char* pArgs[ ] )

{

Student ms ;

fn( ms ) ;

return 0 ;

}

И в самом деле, почему бы не объявить копирующий конструктор класса Student как Student::Student( Student ) ? Однако такое объявление попросту невозможно! При попытке скомпилировать программу с таким объявлением вы получите сообщение об ошибке; Dev-C++ сообщит примерно следующее:

invalid constructor; you probably meant ' Student ( const Student& )'

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

_________________

223 стр. Глава 18. Копирующий конструктор

Глава 19. СТАТИЧЕСКИЕ ЧЛЕНЫ...224

ОГЛАВЛЕНИЕ

В этой главе...

►Определение статических членов 224

►Объявление статических функций-членов 228

►Что такое this 230

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

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

►Определение статических членов...224

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

Зачем нужны статические члены...224

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

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

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

_________________

224 стр. Часть 3. Введение в классы

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

[Советы]

Использование статических членов...225

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

class Student

{

public :

Student( char *pName = "no name" ) : name( pName )

{

noOfStudents++ ;

}

~Student( )

{

noOfStudents-- ;

}

protected :

static int noOfStudents ;

string name ;

} ;

Student s1 ;

Student s2 ;

Член noOfStudents входит в состав класса Student, но не входит в состав объектов s1 и s2. Таким образом, для любого объекта класса Student существуют отдельные члены name и только один noOfStudents, который доступен для всех объектов класса Student.

"Хорошо,— спросите вы,— если место под noOfStudents не выделено ни в каком объекте класса Student, то где же он находится?" Ответ прост: это место не выделяется. Вы должны сами выделить для него место так, как показано ниже, int Student::noOfStudents = 0 ;

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

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

[Помни!]

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

[Советы]

_________________

225 стр. Глава 19. Статические члены

Обращение к статическим данным-членам...226

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

class Student

{

public :

Student( )

{

noOfStudents++ ; /* Обращение из класса */

/* ...остальная программа... */

}

static int noOfStudents ;

/* ...то же, что и раньше... */

} ;

void fn( Student& s1 , Student s2 )

{

/* Обращение к открытому статическому члену */

cout << "Количество студентов - "

<< s1.noOfStudents /* Обращение извне Класса */

<< "\n" ;

}

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

/* ...Класс определяется так же, как и раньше... */

void fn( Student& s1 , Student s2 )

{

/* Представленные команды приведут к идентичному результату */

cout << "Количество студентов - "

<< s1.noOfStudents <<"\n" ;

cout << "Количество студентов - "

<< s2.noOfStudents << "\n" ;

}

На самом деле нам вообще не нужен объект! Можно использовать просто имя класса, как показано в следующем примере:

/* ...Класс определяется так же, как и раньше... */

void fn( Student& s1 , Student s2 )

{

/* Результат остаётся неизменным */

cout << "Количество студентов - "

<< Student::noOfStudents

<< " \ n" ;

}

Независимо от того, будете ли вы использовать имя объекта или нет, С++ всё равно будет использовать имя класса.

_________________

226 стр. Часть 3. Введение в классы

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

[Технические подробности]

class Student

{

static int noOfStudents ;

Student& nextStudent( ) ;

/* ...To же, что и раньше... */

} ;

void fn( Student& s )

{

cout << s.nextStudent( ).noOfStudents

<< "\n" ;

}

Функция-член nextStudent( ) в этом примере не должна вызываться. Всё, что нужно знать С++ для обращения к noOfStudents, — тип возвращаемого значения, а он может это выяснить и не выполняя данную функцию[ 15 ].

Применение статических данных-членов...227

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

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

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

___________________

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

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

_________________

227 стр. Глава 19. Статические члены

►Оъявление статических функций-членов...228

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

«Таким образом, оба вызова статической функции-члена number( ) в приведённой ниже программе корректны.»

[Диск]

//

/* CallStaticMember — два способа вызова */

/*               статической функции-члена */

//

#include <cstdio>

#include <cstdlib>

#include <iostream>

#include <strings.h>

using namespace std ;

class Student

{

public :

Student( char* pN = "no name" )

{

pName = new char[ strlen( pN ) + 1 ] ;

if ( pName )

{

strcpy( pName , pN ) ;

}

noOfStudents++ ;

}

~Student ( ) { noOfStudents-- ; }

static int number( ) { return noOfStudents ; }

/* ...Всё прочее — как и раньше... */

protected :

char* pName ;

static int noOfStudents ;

} ;

int Student::noOfStudents = 0 ;

int main( int argcs , char* pArgs[ ] )

{

/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */

setlocale ( LC_ALL , ".1251" ) ;

Student s1( "Chester" ) ;

Student s2 ( "Scooter" ) ;

cout << "Количество студентов — "

<< s1.number( ) << endl ;

cout << " Количество студентов — "

<< Student::number( ) << endl ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ;

return 0 ;

}

Количество студентов — 2

Количество студентов — 2

Press any key to continue...

_________________

228 стр. Часть 3. Введение в классы

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

class Student

{

public :

/* Приведённый ниже код неверен */

static char *sName( )

{

return pName ; /* Какое именно имя? */

}

/* ...всё остальное то же, что и ранее... */

protected :

char * pName ;

static int noOfStudents ;

} ;

Это не означает, что статические функции-члены не имеют доступа к нестатическим данным-членам. Рассмотрим следующий пример, в котором функция findName( ) используется для поиска объекта в связанном списке ( о том, как работают связанные списки, рассказывается в главе 14, "Указатели на объекты" ). Здесь приводится только относящаяся к нашему рассмотрению часть кода; всё остальное вы можете дописать самостоятельно, в качестве небольшого домашнего задания.

class Student

{

public :

Student ( char *pName )

{

/* Создаёт объект и добавляет его в список */

}

static Student* findName( char *pName )

{

/* Ищет объект в списке, на который указывает указатель pHead */

}

protected :

static Student *pHead ;

Student *pNext ;

char* pName ;

} ;

Student* Student::pHead = 0

Функция findName( ) имеет доступ к pHead, поскольку этот указатель доступен для всех объектов. Так как findName является членом класса Student, он имеет доступ к членам pNext объектов. Этот доступ позволяет функции проходить по списку в поисках требуемого объекта. Вот как используется такая функция.

int main( int argcs , char* pArgs[ ] )

{

Student s1( "Randy" ) ;

Student s2( "Jenny" ) ;

Student s3( "Kinsey" ) ;

Student *pS = s1.findName( "Jenny" ) ;

return 0 ;

}

_________________

229 стр. Глава 19. Статические члены

►Что такое this...230

Я уже упоминал несколько раз о том, что такое this, но тем не менее давайте ещё раз разберёмся в этом вопросе, this — это указатель на текущий объект внутри функции-члена. Он используется, когда не указано другое имя объекта. В обычной функции-члене this — скрытый первый аргумент, передаваемый функции.

class SC

{

public :

void nFn( int a ) ;

/* To же, что и SC::nFn( SC *this , int a ) */

static void sFn( int a ) ;

/* To же, что и SC::sFn( int a ) */

} ;

void fn( SC& s )

{

s.nFn( 10 ) ; /* Преобразуется в SC::nFn( &s , 10 ) ; */

s.sFn( 10 ) ; /* Преобразуется в SC::sFn( 10 ) ; */

}

Таким образом, функция nFn( ) интерпретируется так же, как если бы мы объявили её void SC::nFn( SC *this , int a ). При вызове nFn( ) неявным первым аргументом ей передаётся адрес s ( вы не можете записать вызов таким образом, поскольку передача адреса объекта — дело компилятора ).

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

_________________

230 стр. Часть 3. Введение в классы

Часть 4. НАСЛЕДОВАНИЕ...231

ОГЛАВЛЕНИЕ

СОДЕРЖАНИЕ

Глава 20. НАСЛЕДОВАНИЕ КЛАССОВ...233

Глава 21. ЗНАКОМСТВО С ВИРТУАЛЬНЫМИ ФУНКЦИЯМИ-ЧЛЕНАМИ: НАСТОЯЩИЕ ЛИ ОНИ...240

Глава 22. РАЗЛОЖЕНИЕ КЛАССОВ...249

В этой части...

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

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

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

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

_________________

232 стр. Часть 4. Наследование

Глава 20. НАСЛЕДОВАНИЕ КЛАССОВ...233

ОГЛАВЛЕНИЕ

В этой главе...

►Зачем нужно наследование 234

►Как наследуется класс 234

►Отношение СОДЕРЖИТ 238

В этой главе обсуждается наследование ( inheritance ), т.е. способность одного класса наследовать возможности или свойства другого класса. Наследование — это общепринятая концепция. Я — человек ( за исключением раннего утра... ). И я наследую некоторые свойства класса Человек, например возможность говорить ( в большей или меньшей степени ), интеллект ( надеюсь, что в большей степени ), необходимость в воздухе, воде, пище и разных витаминах. Эти свойства не являются уникальными для каждого отдельного человека. Очевидно, что класс Человек наследует зависимость от воды, воздуха и пищи у класса Млекопитающие, который, в свою очередь, наследует эти свойства у класса Животные.

Концепция, в основе которой лежит способность передавать свойства по наследству, очень мощная. Благодаря ей можно значительно сэкономить место при описании реального объекта. Например, если мой сын спросит: "Что такое утка?", я смогу сказать: "Это птица, которая крякает". Несмотря на краткость, этот ответ несёт в себе всю необходимую для описания утки ( по крайней мере, для моего сына ) информацию. Мой сын знает, что такое птица, и может понять, что утке присущи все свойства птицы плюс свойство "кряканье".

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

class Student

{

} ;

class GraduateStudent : public Student

{

} ;

В этом примере GraduateStudent наследует все члены класса Student. Таким образом, GraduateStudent ЯВЛЯЕТСЯ студентом ( использование прописных букв должно подчеркнуть важность этого отношения ). Конечно, при этом GraduateStudent может также содержать уникальные, присущие именно ему члены.

_________________

233 стр. Глава 20. Наследование классов

►Зачем, нужно наследование...234

Наследование было включено в С++ по нескольким причинам. Конечно, основной из них была необходимость выражать связи между классами с помощью наследования ( к этому я ещё вернусь ). Менее важной целью было уменьшение размера исходного кода. Представьте себе, что у вас есть класс Student и вас попросили добавить новый класс под названием GraduateStudent. В этом случае наследование значительно уменьшит количество членов, которые вам придётся добавлять в класс. Всё, что вам действительно нужно в классе GraduateStudent, — это члены, которые будут описывать отличия между студентами и аспирантами.

¦¦¦¦¦¦¦¦¦¦«

Это потрясающе

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

Если использовать другой пример, можно сказать, что студент является человеком ( точнее, его частным случаем ). Как только это сказано, я уже знаю довольно много о студентах ( об американских студентах, естественно ). Я знаю, что они имеют номера социального страхования, что они слишком много смотрят телевизор и постоянно мечтают о сексе. Я знаю всё это потому, что это свойства всех людей. В С++ мы говорим, что класс student наследует класс Person. Кроме того, мы говорим, что Person является базовым классом для класса student. Наконец, мы говорим, что student ЯВЛЯЕТСЯ Person ( использование прописных букв — общепринятый метод отражения уникального типа связи; не я это придумал ). Эта терминология используется в С++ и других объектно-ориентированных языках программирования.

Заметьте, что хотя Student и ЯВЛЯЕТСЯ Person, обратное не верно. Person не ЯВЛЯЕТСЯ Student ( такое выражение следует трактовать в общем смысле, поскольку конкретный человек, конечно же, может оказаться студентом ). Существует много людей, которые являются членами класса Person и не являются членами класса student. Кроме того, класс student имеет средний балл, a Person его не имеет.

Свойство наследования транзитивно. Например, если я определю новый класс GraduateStudent как подкласс класса student, то он тоже будет наследником person. Это значит, что будет выполняться следующее: если GraduateStudent ЯВЛЯЕТСЯ Student и Student ЯВЛЯЕТСЯ Person, то GraduateStudent ЯВЛЯЕТСЯ Person.

»¦¦¦¦¦¦¦¦¦¦

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

►Как наследуется класс...234

«Здесь приведён пример уже рассмотренного класса GraduateStudent, который дополнен несколькими членами.»

[Диск]

/* InheritanceExample — пример наследования, при */

/*                  котором конструктор наследника */

/*   передаёт информацию конструктору базового класса */

_________________

234 стр. Часть 4. Наследование

#include <cstdio>

#include <cstdlib>

#include <iostream>

#include <strings.h>

using namespace std ;

/* Advisor — пустой класс */

class Advisor { } ;

const int MAXNAMESIZE = 40 ;

class Student

{

public :

Student( char *pName = "no name" )

: average( 0.0 ) , semesterHours( 0 )

{

strncpy( name , pName , MAXNAMESIZE ) ;

name[ MAXNAMESIZE - 1 ] = '\0' ;

cout << "Конструктор Student "

<< name

<< endl ;

}

void addCourse( int hours , float grade )

{

cout << "Добавляем оценку для " << name << endl ;

average = ( semesterHours * average + grade ) ;

semesterHours += hours ;

average = average / semesterHours ;

}

int hours( ) { return semesterHours ; }

float gpa( ) { return average ; }

protected :

char name[ MAXNAMESIZE ] ;

int semesterHours ;

float average ;

} ;

class GraduateStudent : public Student

{

public :

GraduateStudent( char *pName , Advisor& adv , float qG = 0.0 )

: Student( pName ), advisor( adv ) , qualifierGrade(qG)

{

cout << "Конструктор GraduateStudent "

<< pName

<< endl ;

}

float qualifier( ) { return qualifierGrade ; }

protected :

Advisor advisor ;

_________________

235 стр. Глава 20. Наследование классов

float qualifierGrade ;

} ;

int main( int nNumberofArgs , char* pszArgs[ ] )

{

/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */

setlocale ( LC_ALL ,".1251" ) ;

Advisor advisor ;

/* Создание двух типов студентов */

Student llu( "Су N Sense" ) ;

GraduateStudent gs( "Matt Madox" , advisor , 1.5 ) ;

/* Добавляем им оценки */

llu.addCourse( 3 , 2.5 ) ;

gs.addCourse( 3 , 3.0 ) ;

// Выводим их

cout << "Оценка Matt = "

<< gs.qualifier( )

<< endl ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

В этой программе продемонстрировано создание и использование двух объектов — Student и GraduateStudent. Вывод программы выглядит следующим образом.

Конструктор Student Су N Sense

Конструктор Student Matt Madox

Конструктор GraduateStudent Matt Madox

Добавляем оценку для Су N Sense

Добавляем оценку для Matt Madox

Оценка Matt = 1.5

Press any key to continue...

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

Класс Student определён как обычно. Определение класса GraduateStudent несколько отличается — наличием после имени класса двоеточия с последующим public Student. Тем самым класс GraduateStudent объявляется как подкласс класса Student.

«Ключевое слово public говорит о том, что может быть наследование protected, а также private — но эти вопросы лежат за пределами данной книги.»

[Советы]

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

■■■

■ GraduateStudent — подкласс Student;

■ Student — базовый, или родительский класс для GraduateStudent;

■ GraduateStudent наследует Student;

■ GraduateStudent расширяет Student.

■■■

_________________

236 стр. Часть 4. Наследование

В качестве подкласса Student класс GraduateStudent наследует все его члены. Например, GraduateStudent имеет член name, хотя он объявлен в базовом классе. Однако подкласс может добавлять собственные члены, например, qualifierGrade.

Функция main( ) объявляет два объекта, типа Student и GraduateStudent, после чего вызывает функцию addCourse( ) для каждого из них, а потом — функцию qualifier( ), которая имеется только у подкласса.

Конструирование подкласса...237

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

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

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

GraduateStudent( char *pName , Advisor&adv , float qG=0.0 )

:Student( pName ) , advisor( adv ) , qualifierGrade(qG)

{

/* Код конструктора */

}

В этом примере конструктор класса GraduateStudent вызывает конструктор Student, передавая ему аргумент pName. Базовый класс конструируется до любых объектов-членов, а значит, конструктор класса Student вызывается перед конструктором Advisor. И только после конструктора члена Advisor начинает работу конструктор GraduateStudent.

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

class Pig

{

public :

Pig( ) : pHouse( 0 ) { }

protected :

House* pHouse ;

} ;

class LittlePig : public Pig

{

public :

LittlePig( float volStraw , int numSticks , int numBricks )

: straw( volStraw ) , sticks( numSticks ) , bricks( numBricks )

{ }

protected :

float straw ;

int sticks ;

int bricks ;

} ;

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

_________________

237 стр. Глава 20. Наследование классов

Деструкция подкласса...238

Следуя правилу о том, что деструкторы вызываются в порядке, обратном вызову конструкторов, первым вызывается деструктор GraduateStudent. После того как он выполнит свою работу, управление передаётся деструктору класса Advisor, а затем деструктору Student. Если бы Student был наследником класса Person, его деструктор получил бы управление после деструктора Student.

И это логично. Блок памяти сначала преобразуется в объект Student, а уже затем конструктор для GraduateStudent превращает этого студента в аспиранта. Деструктор же просто выполняет этот процесс в обратном направлении.

►Отношение СОДЕРЖИТ...238

Обратите внимание, что класс GraduateStudent включает в себя члены классов Student и Advisor, однако он включает их по-разному. Определяя данные-члены класса Advisor, вы знаете, что класс Student содержит внутри все данные-члены класса Advisor, но вы не можете сказать, что GraduateStudent ЯВЛЯЕТСЯ Advisor. Однако вы можете сказать, что GraduateStudent СОДЕРЖИТ Advisor. Какая разница между этим отношением и наследованием?

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

Если друзья пригласят вас приехать на воскресный пикник на новой машине и вы приедете на ней, никто не будет удивлён ( даже если вы явитесь на мотоцикле ), поскольку автомобиль ЯВЛЯЕТСЯ транспортным средством. Но если вы появитесь на своих двоих, неся в руках мотор, друзья решат, что вы попросту издеваетесь над ними, поскольку мотор не является транспортным средством, так как не имеет некоторых важных свойств, присущих транспортным средствам.

В аспекте программирования связь типа СОДЕРЖИТ достаточно очевидна. Разберём следующий пример:

class Vehicle

{

} ;

class Motor

{

} ;

class Car : public Vehicle

{

public :

Motor motor ;

} ;

void VehicleFn( Vehicle& v ) ;

void motorFn( Motor& m ) ;

_________________

238 стр. Часть 4. Наследование

int main( )

{

Car с ;

VehicleFn( с ) ; /* Так можно вызвать */

motorFn( c ) ; /* А так — нельзя */

motorFn( с.motor ) ; /* Нужно вот так */

return 0 ;

}

Вызов VehicleFn( с ) допустим, поскольку с ЯВЛЯЕТСЯ Vehicle. Вызов motorFn( с ) недопустим, поскольку с — не Motor, хотя он и содержит Motor. Если возникает необходимость передать функции только ту часть с, которая является мотором, это следует выразить явно: motorFn( с.motor ).

_________________

239 стр. Глава 20. Наследование классов

Глава 21. ЗНАКОМСТВО С ВИРТУАЛЬНЫМИ ФУНКЦИЯМИ-ЧЛЕНАМИ: НАСТОЯЩИЕ ЛИ ОНИ...240

ОГЛАВЛЕНИЕ

В этой главе...

►Зачем нужен полиморфизм 243

►Как работает полиморфизм 245

►Когда функция не является виртуальной 246

►Виртуальные особенности 247

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

void someFn( int )

void someFn( char* )

void someFn( char* , double )

Во всех трёх случаях функции имеют одинаковое короткое имя someFn( ). Полные имена всех трёх функций различаются: someFn( int ) отличается от someFn( char* ) и т.д. С++ решает, какую именно функцию нужно вызвать, рассматривая полные имена слева направо.

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

[Атас!]

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

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

class Student

{

public :

float calcTuition( ) ;

} ;

class GraduateStudent : public Student

{

public :

float calcTuition( ) ;

} ;

int main( int argcs , char* pArgs[ ] )

{

Student s ;

GraduateStudent gs ;

s.calcTuition( ) ; /* Вызывает Student::calcTuition( ) */

gs.calcTuition( ) ; /* Вызывает  GraduateStudent::calcTuition( ) */

return 0 ;

}

_________________

240 стр. Часть 4. Наследование

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

Но что, если класс объекта не может быть точно определён на этапе компиляции? Чтобы продемонстрировать подобную ситуацию, нужно просто немного изменить приведённую выше программу:

//

/* OverloadOverride — демонстрация невозможности  */

/*                    точного определения типа */

//

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

class Student

{

public :

/* Раскомментируйте одну из двух следующих строк; одна выполняет раннее связывание calcTuition( ), а вторая — позднее  */

float calcTuition( )

/* virtual float calcTuition( ) */

{

cout << "Функция Student::calcTuition" << endl ;

return 0 ;

}

} ;

class GraduateStudent : public Student

{

public :

float calcTuition( )

{

cout << "Функция GraduateStudent::calcTuition"

<< endl ;

return 0 ;

}

} ;

void fn( Student& x )

{

x.calcTuition( ) ; /* Какая функция calcTuition( ) должна быть вызвана? */

}

_________________

241 стр. Глава 21. Знакомство с виртуальными функциями-членами: настоящие ли они

int main( int nNumberofArgs , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

/* Передача функции объекта базового класса */

Student s ;

fn( s ) ;

/* Передача функции объекта подкласса */

GraduateStudent gs ;

fn( gs ) ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

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

Функция Student::calcTuition

Функция Student::calcTuition

Press any key to continue...

На этот раз вместо прямого вызова calcTuition( ) осуществляется вызов через промежуточную функцию fn( ). Теперь всё зависит от того, какой аргумент передаётся fn( ), поскольку х может быть как Student, так и GraduateStudent — ведь GraduateSudent ЯВЛЯЕТСЯ Student!

«Если вы этого не знали, это вовсе не говорит о том, что вы ЯВЛЯЕТЕСЬ "чайником". Это значит, что вы не читали главу 20 , "Наследование классов".»

[Помни!]

Аргумент х, передаваемый fn( ), для экономии места и времени объявлен как ссылка на объект класса Student. Если бы этот аргумент передавался по значению, С++ пришлось бы при каждом вызове fn( ) конструировать новый объект Student. В зависимости от вида класса Student и количества вызовов fn( ) в итоге это может занять много времени, тогда как при вызове fn( Student& ) или fn( Student* ) передаётся только адрес. Если вы не поняли, о чём я говорю, перечитайте главу 18, "Копирующий конструктор".

Было бы неплохо, если бы строка х.calcTuition( ) вызывала Student::calcTuition( ), когда х является объектом класса Student, и GraduateSudent::calcTuition( ), когда х является объектом класса GraduateStudent. Если бы С++ был настолько "сообразителен", это было бы действительно здорово! Почему? Об этом вы узнаете далее в главе.

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

В данном случае объявленный тип аргумента функции fn( ) не полностью описывает требования к функции. Хотя аргумент и объявлен как Student, он может оказаться также и GraduateStudent. Окончательное решение можно принять, только когда программа выполняется ( это называется "на этапе выполнения" ). И только когда функция fn( ) уже вызвана, С++ может посмотреть на тип аргумента и решить, какая именно функция-член должна вызываться: из класса Student или из GraduateStudent.

_________________

242 стр. Часть 4. Наследование

«Типы аргументов, с которыми вы сталкивались до этого времени, называются объявленными, или типами этапа компиляции. Объявленным типом аргумента х в любом случае является Student, поскольку так написано в объявлении функции fn( ). Другой, текущий, тип называется типом этапа выполнения. В случае с примером функции fn( ) типом этапа выполнения аргумента х является Student, если fn( ) вызывается с s, и GraduateStudent, когда fn( ) вызывается с gs.»

[Советы]

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

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

►Зачем нужен полиморфизм...243

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

«Языки, которые поддерживают классы, но не поддерживают полиморфизм, называются объектно-основанными. К таким языкам относится, например, Ada.»

[Советы]

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

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

void someFunction( Student& s )

{

/* ...то, что эта функция должна делать... */

s.calcTuition( ) ;

/* ...функция продолжается... */

}

Если бы С++ не поддерживал позднее связывание, мне бы пришлось отредактировать функцию someFunction( ) приблизительно так, как показано ниже, и добавить её в класс GraduateStudent.

_________________

243 стр. Глава 21. Знакомство с виртуальными функциями-членами: настоящие ли они

#define STUDENT 1

#define GRADUATESTUDENT 2

void someFunction( Student& s )

{

/* ...то, что эта функция должна делать... Добавим тип члена, который будет индицировать текущий тип объекта */

switch ( s.type )

{

case STUDENT :

s.Student::calcTuition( ) ;

break ;

case GRADUATESTUDENT :

s.GraduateStudent::calcTuition( ) ;

break ;

}

/* ...функция продолжается... */

}

Мне бы пришлось добавить в класс переменную type. После этого я был бы вынужден добавить присвоения type = STUDENT к конструктору Student и type = GRADUATESTUDENT к конструктору GraduateStudent. Значение переменной type отражало бы текущий тип объекта s. Затем мне пришлось бы добавить проверяющие команды, показанные в приведённом выше фрагменте программы, везде, где вызываются переопределяемые функции.

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

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

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

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

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

На некотором философском уровне есть ещё более важные причины для полиморфизма. Помните, как я готовил закуски в микроволновой печи? Можно сказать, что я действовал по принципу позднего связывания. Рецепт был таким: разогрейте закуску в печи. В нём не было сказано: если печь микроволновая, сделай так, а если конвекционная — эдак. В рецепте ( читай — коде ) предполагалось, что я ( читай — тот, кто осуществляет позднее связывание ) сам решу, какой именно разогрев ( функцию-член ) выбрать, в зависимости от типа используемой печи ( отдельного экземпляра класса Oven ) или её вариаций ( подклассов ), например таких, как микроволновая печь ( Microvawe ). Так думают люди, и так же создаются языки программирования: чтобы дать людям возможность, не изменяя образа мыслей, создавать более точные модели реального мира.

_________________

244 стр. Часть 4. Наследование

►Как работает полиморфизм...245

Любой язык программирования может поддерживать раннее либо позднее связывание. Старые языки типа С в основном поддерживают раннее связывание. Более поздние языки, наподобие Java, поддерживают позднее связывание. С++ же поддерживает оба типа связывания.

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

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

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

class Student

{

public :

/* Раскомментируйте одну из двух следующих строк; одна выполняет раннее связывание calcTuition( ), а вторая — позднее */

virtual float calcTuition( )

{

cout << "Функция Student::calcTuition" << endl ;

return 0 ;

}

} ;

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

В приведённой ранее демонстрационной программе OverloadOverride calcTuition( ) вызывается через промежуточную функцию fn( ). Когда функции fn( ) передаётся объект базового класса, она вызывает функцию Student::calcTuition( ). Но когда функции передаётся объект подкласса, этот же вызов обращается к функции GraduateStudent::calcTuition( ).

Запуск программы приведёт к выводу на экран таких строк:

Функция Student::calcTuition

Функция GraduateStudent::calcTuition

Press any key to continue...

«Если вы уже освоились с отладчиком вашей среды С++, настоятельно рекомендую выполнить этот пример в пошаговом режиме.»

[Технические подробности]

_________________

245 стр. Глава 21. Знакомство с виртуальными функциями-членами: настоящие ли они

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

[Советы]

«Обратитесь к программе PolymorphicNachos.срр на прилагаемом компакт-диске, чтобы лучше ознакомиться с полиморфизмом.»

[Диск]

►Когда фунция не являтся виртуальной...246

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

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

class Base

{

public :

/* Возвращает копию текущего объекта */

Base* makeACopy( )

{

/* ...делает всё, что нужно для создания копии */

}

} ;

class Subclass : public Base

{

public :

/* Возвращает копию текущего объекта */

Subclass* makeACopy( )

{

/* ...Делает всё, что нужно для создания копии */

}

} ;

void fn( BaseClass& bс )

{

BaseClass* pCopy = bс.makeACopy( ) ;

/* Функция продолжается... */

}

С практической точки зрения всё естественно: функция копирования makeCopy( ) должна возвращать указатель на объект типа Subclass, даже если она переопределяет Base::makeCopy( ).

_________________

246 стр. Часть 4. Наследование

►Виртуальные особенности...247

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

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

Например, приведённый ниже вызов обращается к Base::fn( ), поскольку так указал программист, независимо от того, объявлена fn( ) виртуальной или нет.

void test( Base& b )

{

b.base::fn( ) ;

/* Этот вызов не использует позднего связывания */

}

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

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

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

class Base

{

public :

~Base( ) ;

} ;

class SubClass : public Base

{

public :

~SubClass( ) ;

} ;

void finishWithObject( Base* pHeapObject )

{

delete pHeapObject ; /* Здесь вызывается ~Base( ) независимо от типа указателя pHeapObject */

}

Если указатель, передаваемый функции finishWithObject( ), на самом деле указывает на объект SubСlass, деструктор Subclass всё равно вызван не будет: поскольку он не был объявлен виртуальным, используется раннее связывание. Однако, если объявить деструктор виртуальным, проблема будет решена.

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

_________________

247 стр. Глава 21. Знакомство с виртуальными функциями-членами: настоящие ли они

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

■■■

■ класс не содержит много данных, так что даже один указатель существенно увеличивает его размер;

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

■■■

Если выполняются два перечисленных условия, а в классе нет виртуальных функций — это единственное основание не делать деструктор виртуальным.

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

[Атас!]

_________________

248 стр. Часть 4. Наследование

Глава 22. РАЗЛОЖЕНИЕ КЛАССОВ...249

ОГЛАВЛЕНИЕ

В этой главе...

►Разложение 249

►Реализация абстрактных классов 253

►Разделение исходного кода С++ 259

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

Главное преимущество наследования — возможность указывать тип взаимосвязи между классами. Это так называемая взаимосвязь типа ЯВЛЯЕТСЯ: микроволновая печь ЯВЛЯЕТСЯ печью и т.д.

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

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

►Разложение...249

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

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

[Диск]

Я мог бы до посинения рассказывать об этих классах, однако, к счастью, объектно-ориентированные программисты придумали довольно наглядный и краткий путь описания классов. Классы Checking и Savings показаны на рис. 22.1.

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

■■■

■ Большой прямоугольник — это класс. Имя класса написано сверху.

■ Имена в меньших прямоугольниках — это функции-члены.

_________________

249 стр. Глава 22. Разложение классов

Рис. 22.1. Независимые классы Checking и Savings

■ Имена не в прямоугольниках — это данные-члены.

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

■ Толстая стрелка обозначает связь типа ЯВЛЯЕТСЯ.

■ Тонкая стрелка обозначает связь типа СОДЕРЖИТ.

■■■

Автомобиль ЯВЛЯЕТСЯ транспортным средством и при этом СОДЕРЖИТ мотор.

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

Можно сделать так, чтобы один из этих классов наследовал другой. Класс Savings имеет больше членов, чем Checking, так что мы могли бы унаследовать Savings от Checking. Такой путь реализации этих классов приведён на рис. 22.2. Класс Savings наследует все члены класса Checking. Кроме того, в классе добавлен член noWithdrawal и переопределена функция withdrawal( ). Эта функция переопределена, поскольку правила снятия денег со сберегательного счёта отличаются от правил снятия с чекового счёта ( хотя меня эти правила вообще не касаются, поскольку у меня нет денег, которые можно было бы снять со счёта ).

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

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

_________________

250 стр. Часть 4. Наследование

Рис. 22.2. Класс Savings реализован как подкласс checking

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

Такое изменение политики банка можно легко отразить в классе Checking. Все, что нужно сделать, — это добавить новый член в класс Checking, чтобы следить за минимальным балансом в течение месяца. Назовём его minimumBalance.

Однако теперь возникает проблема. Если Savings наследует Checking, значит, Savings тоже получает этот член. При этом он не используется, поскольку в сберегательных счетах минимальный баланс не нужен. Так что дополнительный член просто присутствует в классе. Итак, каждый объект чекового счёта имеет дополнительный член minimumBalance. Один дополнительный член — это не так уж и много, но он вносит свою лепту в общую неразбериху.

Такие изменения имеют свойство накапливаться. Сегодня это один член, а завтра — изменённая функция-член. В результате объекты класса Savings будут содержать множество дополнительных данных, которые нужны исключительно в классе Checking. Если вы будете невнимательны, изменения в классе Checking могут перейти к классу Savings и привести к его некорректной работе.

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

Как же этого избежать? Если поменять местами Checking и Savings, проблема не исчезнет. Нужен некий третий класс ( назовём его Account ), который будет воплощать в себе всё то общее, что есть у Checking и Savings. Такая связь приведена на рис. 22.3.

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

_________________

251 стр. Глава 22. Разложение классов

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

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

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

Рис. 22.3. Классы checking и Savings, базирующиеся на классе Account

«Разложение будет обоснованным только в том случае, когда взаимосвязь, представляемая наследованием, соответствует реальности. Выделение общих свойств класса Mouse и Joystick и разложение их на "множители" вполне допустимо. И мышь и джойстик являются аппаратными устройствами позиционирования. Но выделение общих свойств классов Mouse и Display ничем не обосновано.»

[Атас!]

Разложение может давать ( и обычно даёт ) результат на нескольких уровнях абстракции. Например, программа, написанная для более "продвинутого" банка, может иметь структуру классов, показанную на рис. 22.4.

Из этого рисунка видно, что между классами Checking и Savings и более общим классом Account вставлен ещё один класс. Он называется Conventional и объединяет в себе особенности обычных счетов. Другие типы счетов, например счета ценных бумаг и биржевые счета, также объявляются как отдельные классы.

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

_________________

252 стр. Часть 4. Наследование

Рис. 22.4. Развитая структура банковских счетов

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

Рис. 22.5. Альтернативная иерархия классов

►Реализация абстрактных классов...253

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

Большинство функций-членов класса Account не составят проблем, поскольку оба типа счетов реализуют их одинаково. Однако функция Account.withdrawal( ) отличается в зависимости от типа счёта. Правила снятия со сберегательного и чекового счетов различны. Мы вынуждены реализовывать Savings::withdrawal( ) не так, как Checking::withdrawal( ). Но как реализовать функцию Account::withdrawal( ) ?

Попросим банковского служащего помочь нам. Я так представляю себе эту беседу:

"Каковы правила снятия денег со счёта?" — спросите вы с надеждой.

"Какого именно счёта, сберегательного или чекового?" — ответит он вопросом на вопрос.

"Со счёта, — скажете вы, — просто со счёта!"

_________________

253 стр. Глава 22. Разложение классов

Пустой взгляд в ответ...

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

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

Чтобы объяснить, что я имею в виду, позвольте позаимствовать пример из мира животных. Наблюдая разные особи теплокровных и живородящих, вы можете заключить, что они все укладываются в концепцию под названием "млекопитающие". Вы можете выделить такие классы млекопитающих, как собачьи, кошачьи и приматы. Однако невозможно найти где-либо на земле просто млекопитающее. Другими словами, млекопитающие не могут содержать особь под названием "млекопитающее". Млекопитающее — это концепция высокого уровня, которую создал человек, и экземпляров-млекопитающих не существует.

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

Отражая эту ситуацию, С++ даёт возможность оставлять абстрактные классы незавершёнными.

Концепция абстрактных классов...254

Абстрактный класс — это класс с одной или несколькими чисто виртуальными функциями. Прекрасно, это всё разъясняет...

Ну хорошо, чисто виртуальная функция — это функция-член без тела функции ( которого нет, например, потому, что никто не знает, как реализовать это самое тело ).

Бессмысленно спрашивать о том, каким должно быть тело функции withdrawal( ) в классе Account. Хотя, конечно, сама концепция снятия денег со счёта имеет смысл. Программист на С++ может написать функцию withdrawal( ), которая будет отражать концепцию снятия денег со счёта, но при этом данная функция не будет иметь тела, поскольку мы не знаем, как её реализовать. Такая функция называется чисто виртуальной[ 17 ] ( не спрашивайте меня, откуда взялось это название ).

Синтаксис объявления чисто виртуальной функции показан в приведённом ниже классе Account.

/* Account — это абстрактный класс */

class Account

{

protected :

Account( Account& с ) ;

public :

Account( unsigned accNo , float initialBalance = 0.0F ) ;

/* Функции доступа */

unsigned int accountNo( ) ;

float acntBalance( ) ;

static int noAccounts( ) ;

static Account *first( ) ;

Account *next( ) ;

/* Функции транзакций */

void deposit( ) ;

/* Приведённая ниже функция является чисто виртуальной */

virtual void withdrawal( float amount ) = 0 ;

protected :

/* Если хранить счета в связанном списке, не будет ограничения на их количество */

static Account *pFirst ;

Account *pNext ;

static int count ; /* Количество счетов */

unsigned accountNumber ;

float balance ;

} ;

______________

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

_________________

254 стр. Часть 4. Наследование

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

«Я считаю это объяснение глупым, и мне оно нравится не более чем вам, так что просто выучите и живите с ним. Для этого объяснения есть причина, если не оправдание. Каждая виртуальная функция должна иметь свою ячейку в специальной таблице, в которой содержится адрес функции. Так вот: ячейка для чисто виртуальной функции содержит нуль.»

[Технические подробности]

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

void fn( )

{

Account acnt( 1234, 100.00 ) ; /* Это некорректно */

acnt.withdrawal( 50 ) ; /* Куда, по-вашему, должен обращаться этот вызов? */

}

Если бы такое объявление было разрешено, конечный объект оказался бы незавершённым, поскольку был бы лишён некоторых возможностей. Например, что бы выполнял приведённый в этом же объявлении вызов? Помните, функции Account::withdrawal( ) не существует.

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

Создание полноценного класса из абстрактного...255

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

_________________

255 стр. Глава 22. Разложение классов

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

class Display

{

public :

virtual void initialize( ) = 0 ;

virtual void write( char *pString ) = 0 ;

} ;

class SVGA : public Display

{

/* Сделаем обе функции-члена "реальными" */

virtual void initialize( ) ;

virtual void write( char *pString ) ;

} ;

class HWVGA : public Display

{

/* Переопределим только одну функцию */

virtual void write( char *pString ) ;

} ;

class ThreedVGA : public HWVGA

{

virtual void initialize( ) ;

} ;

void fn( )

{

SVGA mc ;

VGA vga ;

/* Всё остальное */

}

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

Один из подклассов — SVGA — не абстрактный. Это отдельный тип видеоадаптера, и программист точно знает, как его реализовать. Таким образом, класс SVGA переопределяет обе функции — initialize( ) и write( ) — именно так, как необходимо для данного адаптера.

Ещё один подкласс — HWVGA . Программисту известно, как программировать ускоренный VGA-адаптер. Поэтому между общим классом Display и его частным случаем, ThreedVGA, который представляет собой специальный тип карт 3-D, находится ещё один уровень абстракции.

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

_________________

256 стр. Часть 4. Наследование

При этом функция initialize( ) не переопределяется, поскольку для разных типов карт HWVGA она реализуется по-разному.

Несмотря на то что функция write( ) переопределена в классе HWVGA, он всё равно остаётся абстрактным, так как функция initialize( ) всё ещё не переопределена.

Поскольку ThreedVGA наследуется от HWVGA, он должен переопределить только одну функцию, initialize( ), для того чтобы окончательно определить адаптер дисплея. Таким образом, функция fn( ) может свободно реализовать и использовать объект класса ThreedVGA.

«Замещение нормальной функцией последней чисто виртуальной функции делает класс завершённым ( т.е. неабстрактным ). Только неабстрактные классы могут быть реализованы в виде объектов.»

[Помни!]

Передача абстрактных классов...257

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

void fn( Account *pAccount ) ; /* Это допустимо */

void otherFn( )

{

Savings s ;

Checking c ;

/* Savings ЯВЛЯЕТСЯ Account */

fn( &s ) ;

/* Checking — тоже */

fn( &c ) ;

}

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

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

Нужны ли чисто виртуальные функции...257

Если нельзя определить функцию withdrawal( ), почему бы просто не опустить её? Почему бы не объявить её в классах Savings и Checking, где она может быть определена, оставив в покое класс Account? Во многих объектно-ориентированных языках вы могли бы именно так и сделать. Но С++ предпочитает иметь возможность убедиться в вашем понимании того, что вы делаете.

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

[Помни!]

_________________

257 стр. Глава 22. Разложение классов

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

class Account

{

/* То же, что и раньше, но  нет функции withdrawal( ) */

} ;

class Savings : public Account

{

public :

virtual void withdrawal( float amnt ) ;

} ;

void fn( Account *pAcс )

{

/* снять некоторую сумму */

pAcc -> withdrawal( 100.00f ) ;

/* Этот вызов недопустим, поскольку withdrawal( )не является членом класса Account */

}

int main( )

{

Savings s ; /* Открыть счёт */

fn( &s ) ;

/* Продолжение программы */

}

Представьте себе, что вы открываете сберегательный счёт s. Затем вы передаёте адрес этого счёта функции fn( ), которая пытается выполнить функцию withdrawal( ). Однако, поскольку функция withdrawal( ) не член класса Account, компилятор сгенерирует сообщение об ошибке.

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

class Account

{

/* Почти то же, что и в предыдущей программе, однако функция withdrawal( ) определена */

virtual void withdrawal( float amnt ) = 0 ;

} ;

class Savings : public Account

{

public :

virtual void withdrawal( float amnt ) ;

} ;

void fn( Account *pAcc )

{

/* Снять некоторую сумму. Теперь этот код будет работать */

рАсс -> withdrawal( 100.00f ) ;

}

int main( )

{

Savings s ; /* Открыть счёт */

fn( &s ) ;

/* Продолжение программы */

}

_________________

258 стр. Часть 4. Наследование

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

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

►Разделение исходного кода С++...259

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

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

«Процесс объединения раздельно скомпилированных модулей в единый выполнимый файл называется компоновкой, или связыванием ( linking ) — линкованием.»

[Технические подробности]

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

«Инкапсуляция представляет собой одно из преимуществ объектно-ориентированного программирования.»

[Помни!]

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

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

И наконец, вопрос времени. Компилятору наподобие Visual С++ .NET или Dev-C++ не требуется много времени, чтобы скомпилировать примеры из этой книги. Однако серьёзные коммерческие программы состоят из миллионов строк кода, и полная компиляция и сборка таких программ может потребовать больше суток машинного времени. Вряд ли программист сможет нормально работать, если после внесения любого изменения ему потребуются сутки на сборку приложения. Гораздо быстрее оказывается перекомпилировать только один файл с внесёнными изменениями, после чего быстро скомпоновать приложение из уже скомпилированных модулей.

_________________

259 стр. Глава 22. Разложение классов

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

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

Разделение программы - класс student...260

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

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

[Советы]

Полученный в результате файл Student.h выглядит следующим образом.

/* Student — базовый класс */

#ifndef _STUDENT_

#define _STUDENT_

namespace Schools

{

class Student

{

public :

Student( char* pszName , int nID ) ;

virtual char* display( ) ;

protected :

/* Имя студента */

char* pszName ;

int nID ;

} ;

}

#endif

#ifndef представляет собой директиву препроцессора, такую же, как и, например, директива #include. Данная директива гласит, что включать следующие за ней строки следует только в том случае, когда её аргумент _STUDENT_ не определён. При первом включении файла _STUDENT_ не определён, но тут же определяется директивой #define_STUDENT_. Это приводит к тому, что включение файла Student.h в программу будет выполнено только один раз, независимо от того, сколько директив #include встретится в программе.

_________________

260 стр. Часть 4. Наследование

Определение пространства имён...261

Следующая особенность программы состоит в том, что класс Student определён в пространстве имён Schools.

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

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

«Ещё одной причиной для использования пространств имён служат так называемые "коллизии имён", которых надо избегать. Например, класс Grade из пространства имён Schools никак не влияет на возможность использования этого же имени для класса в пространстве имён FoodProduction.»

[Советы]

Реализация класса student...261

Реализация класса Student помещена мною в файл Student.срр.

/* Student — реализация методов класса Student */

#include <cstdio>

#include <cstdlib>

#include <iostream>

#include <string.h>

#include "student.h"

namespace Schools

{

Student::Student( char* pszNameArg , int nIDArg )

: nID( nIDArg )

{

pszName = new char[ strlen( pszNameArg ) + 1 ] ;

strcpy( pszName , pszNameArg ) ;

}

/* display — возвращает описание студента */

char* Student::display( )

{

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

char* pReturn = new char[ strlen( pszName ) + 1 ] ;

strcpy( pReturn , pszName ) ;

return pReturn ;

}

}

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

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

_________________

261 стр. Глава 22. Разложение классов

«По историческим причинам в большинстве сред С++ этот промежуточный файл имеет расширение .obj или .о ( "объектный файл" ).»

[Технические подробности]

Разделение программы - класс GraduateStudent...262

Следующий модуль, представляющийся квазинезависимым, — GraduateStudent. Он может быть помещён в файл Student.срр, однако ряд программ могут работать только со студентами, даже не подозревая о наличии аспирантов.

Класс GraduateStudent сделан мною максимально простым. Вот как выглядит заголовочный файл.

/* GraduateStudent — специальный тип Student */

#ifndef _GRADUATE_STUDENT_

#define _GRADUATE_STUDENT_

#include "student.h"

namespace Schools

{

class GraduateStudent : public Student

{

public :

/* Тривиальный конструктор */

GraduateStudent( char* pszName , int nID )

: Student( pszName , nID ){ }

/* Демонстрация виртуальной функции */

virtual char* display( ) ;

} ;

}

#endif

Обратите внимание, что файл GraduateStudent.h включает файл Student.h. Это связано с тем, что класс GraduateStudent зависит от определения класса Student. Файл с исходным кодом содержит реализацию метода display( ).

/* GraduateStudent — специальный тип Student */

#include <cstdio>

#include <cstdlib>

#include <iostream>

#include <string.h>

#include "graduateStudent.h"

namespace Schools

{

char* GraduateStudent::display( )

{

/* Описание студента */

char* pFirst = Student::display( ) ;

/* Добавляем этот текст */

char* pSecond = "-G" ;

/* Выделяем память для новой строки и создаём её */

char* pName = new char[ strlen( pFirst ) +

strlen( pSecond ) + 1 ] ;

strcpy( pName , pFirst ) ;

strcat( pName , pSecond ) ;

/* Освобождаем память, которую вернул вызов Student::display( ) */

delete pFirst ;

return pName ;

}

}

_________________

262 стр. Часть 4. Наследование

Функция display( ) из класса GraduateStudent добавляет "-G" к строке, возвращаемой функцией display( ) из класса Student. Она начинает свою работу с того, что выделяет память из кучи для новой строки.

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

[Атас!]

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

«Забыв освободить память, выделенную в куче, вы получите эффект, именуемый утечкой памяти. Программа с утечкой памяти поначалу работает вполне корректно, но со временем всё большее и большее количество памяти оказывается потерянным, что может привести к неработоспособности программы из-за отсутствия памяти. Обычно утечки памяти очень трудно обнаружить.»

[Атас!]

Реализация приложения...263

Два класса — Student и GraduateStudent — разнесены по отдельным исходным файлам и размещены в пространстве имён Schools. Я написал простенькую программу, использующую оба описанных класса.

//

/* SeparatedMain — демонстрационное приложение, */

/*                 разделённое на части */

//

#include <cstdio>

#include <cstdlib>

#include <iostream>

#include <string.h>

#include "graduateStudent.h"

#include "student.h"

using namespace std ;

/* using namespace Schools ; */

using Schools::GraduateStudent ;

int main( int nArgc , char* pszArgs[ ] )

{

Schools::Student s( "Sophie Moore" , 1234 ) ;

cout << "Student = " << s.display( ) << endl ;

GraduateStudent gs( "Greg U. Waite" , 5678 ) ;

cout << "Student = " << gs.display( ) << endl ;

/* Пауза для того,  чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

_________________

263 стр. Глава 22. Разложение классов

Приложение включает заголовочные файлы Student.h и GraduateStudent.h, что даёт ему доступ к обоим рассмотренным классам.

Вы можете возразить, что включение GraduateStudent.h автоматически приводит к включению Student.h, однако не стоит полагаться на это — если вы хотите использовать какой-то класс, лучше включить соответствующий заголовочный файл независимо от других. Использование описанной ранее конструкции с #ifndef позволит избежать повторного включения заголовочного файла.

Заметим, что представленный модуль не является частью пространства имён Schools, так что при обращении функции main( ) к классу Student С++ не знает, где именно искать этот класс.

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

using Schools::GraduateStudent ;

говорит о том, что далее все упоминания GraduateStudent относятся к пространству имён Schools.

Программист может получить такой же доступ ко всем членам пространства имён сразу, воспользовавшись командой

using namespace Schools ;

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

using namespace Schools ;

int main( int nArgc , char* pszArgs[ ] )

{

Schools::Student s( "Sophie Moore" , 1234 ) ;

cout << "Student = " << s.display( ) << endl ;

GraduateStudent gs( "Greg U. Waite" , 5678 ) ;

cout << "Student = " << gs.display( ) << endl ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

«Использование в данной книге инструкции using namespace std ; связано с тем, что модули, составляющие стандартную библиотеку С++, входят в пространство имён std.»

[Советы]

Файл проекта...264

Полный ожиданий, я открываю файл SeparatedMain.срр в компиляторе и запускаю сборку приложения. Модуль компилируется без ошибок, но компоновка не выполняется. С++ не знает, что за класс Student. Итак, нам надо каким-то образом сообщить компилятору, что кроме SeparatedMain.срр ему нужно использовать файлы GraduateStudent.срр и Student.срр. В большинстве сред С++, включая Dev-C++ и Visual С++ .NET, это делается при помощи файла проекта.

Dev-C++ и Visual С++ .NET имеют собственные форматы файла проекта. Здесь мы рассмотрим, как работать с файлом проекта в Dev-C++.

_________________

264 стр. Часть 4. Наследование

Создание файла проекта в Dev-C++...265

Для создания файла проекта Dev-C++ выполните следующие действия.

1. Выберите команду меню Файл => Создать => Проект ( File => New => Project ). Выберите в диалоговом окне Console Application и введите имя SeparateModules ( рис. 22.6 ).

Рис. 22.6. Выбор типа нового проекта

2. Щёлкните на кнопке ОК.

Dev-C++ откроет окно сохранения файла.

3. Выберите каталог, в котором должен быть сохранён проект.

4. Удалите main.срр из проекта, поскольку у вас уже есть модуль main( ).

5. Выберите команду меню Проект => Удалить из проекта ( Project => Remove From Project ).

6. Выберите main.срр и щёлкните на кнопке ОК.

7. Скопируйте файлы SeparatedMain.cpp, GraduateStudent.срр, Student.срр, Student.h и GraduateStudent.h в рабочую папку ( если они ещё не находятся там ).

8. Выберите команду меню Проект => Добавить к проекту ( Project => Add to Project ).

_________________

265 стр. Глава 22. Разложение классов

9. Выберите все исходные модули и щёлкните на кнопке ОК.

10. Выберите команду меню Выполнить => Перестроить всё ( Execute => Rebuild All ) для компиляции модулей проекта и создания выполнимой программы.

11. Щёлкните на вкладке Классы ( Classes ) в левом окне, чтобы увидеть описания всех классов программы ( рис. 22.7 ).

«Убедитесь, что просмотр классов корректно настроен.»

[Советы]

Рис. 22.7. Описание классов программы

12. Выберите команду меню Сервис => Параметры редактора ( Tools => Editor options ) и щёлкните на вкладке Обзор классов ( Class browsing ).

13. Включите опцию Включить обзор классов ( Enable class browser ) и другие опции, показанные на рис. 22.8.

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

_________________

266 стр. Часть 4. Наследование

Рис. 22.8. Типы информации о классах, доступные для отображения

14. Выберите первую функцию display( ) в списке и щёлкните на маленькой пиктограммке слева от неё.

При этом в окне редактора вы увидите файл Student.срр, причём курсор окажется на функции display( ). Выбор второй функции display( ) перенесёт нас к этой функции в файле GraduateStudent.срр.

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

15. Выберите команду меню Проект => Параметры проекта ( Project => Project Options ).

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

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

_________________

267 стр. Глава 22. Разложение классов

Часть 5. ПОЛЕЗНЫЕ ОСОБЕННОСТИ...269

ОГЛАВЛЕНИЕ

СОДЕРЖАНИЕ

Глава 23. ОПЕРАТОР ПРИСВОЕНИЯ...271

Глава 24. ИСПОЛЬЗОВАНИЕ ПОТОКОВ ВВОДА-ВЫВОДА...277

Глава 25. ОБРАБОТКА ОШИБОК И ИСКЛЮЧЕНИЯ...290

Глава 26. МНОЖЕСТВЕННОЕ НАСЛЕДОВАНИЕ...298

Глава 27. ШАБЛОНЫ С++...308

Глава 28. СТАНДАРТНАЯ БИБЛИОТЕКА ШАБЛОНОВ...317

В этой части...

В этой книге не ставится цель сделать из вас профессионала в области С++, а всего лишь предполагается дать вам твёрдое понимание основ С++ и объектно-ориентированного программирования.

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

Глава 23. ОПЕРАТОР ПРИСВОЕНИЯ...271

ОГЛАВЛЕНИЕ

В этой главе...

►Сравнение операторов и функций 271

►Потоковые операторы 272

►Мелкое копирование — глубокие проблемы 272

►Переопределение оператора присвоения 273

►Защита от копирования 276

Встроенные ( intrinsic ) типы данных — это типы данных, которые компилятор "знает" изначально, такие как int, float, double и другие, а также различные типы указателей. В главах 3, "Выполнение математических операций", и 4, "Выполнение логических операций", были описаны операторы, определённые в С++ для встроенных типов. С++ позволяет программисту определять операторы для создаваемых им классов в дополнение к встроенным операторам. Эта возможность называется перегрузкой операторов.

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

►Сравнение операторов и функций...271

Оператор представляет собой не более чем встроенную функцию с определённым синтаксисом. Так, сложение а+b можно рассматривать, как если бы это была запись operator+( a , b ). С++ даёт каждому оператору имя в стиле функции. Такое функциональное имя оператора состоит из ключевого слова operator, за которым следует символ оператора, а за ним — соответствующие типы аргументов. Например, оператор +, который суммирует целые числа и возвращает целое число, имеет имя int operator+( int , int ).

Любой оператор может быть определён для пользовательского класса. Так, я могу создать Complex operator*( Complex& , Complex& ), который позволит мне умножить два объекта типа Complex. Новый оператор может иметь ту же семантику, что и перегружаемый, но не обязан. При перегрузке операторов действуют следующие правила.

■■■

■ Программист не может перегрузить операторы ., ::, * ( разыменование ) и &.

■ Программист не может вводить новые операторы, например, х$у.

■ Формат оператора не может быть изменён. Например, вы не можете определить оператор %i, поскольку % — бинарный оператор.

■ Приоритет операторов не может быть изменён. Программа не может заставить оператор + выполняться раньше оператора *.

■ Операторы не могут быть переопределены для встроенных типов — вы не в состоянии изменить смысл записи 1+2. Существующие операторы могут быть перегружены только для вновь создаваемых типов.

■■■

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

►Потоковые операторы...272

Операторы считывания из потока и записи в него, << и >>, — это не что иное, как переопределённые операторы левого и правого сдвига для набора классов, представляющих потоки ввода-вывода. Эти определения находятся в файле iostream. Таким образом, запись cout <<"some string" означает вызов функции operator<<( " some string" ). Наши старые знакомые сin и cout представляют собой предопределённые объекты, связанные с клавиатурой и монитором соответственно. Подробнее мы поговорим об этом в главе 24, "Использование потоков ввода-вывода".

►Мелкое копирование — глубокие проблемы...272

Независимо от того, что думаете вы и многие другие о переопределении операторов, вам всё равно придётся переопределять оператор присвоения для множества ваших классов. С++ предоставляет определение operator=( ) по умолчанию, но этот оператор просто выполняет почленное копирование. Такое присвоение отлично работает для встроенных операторов типа int.

int i ;

i = 10 ;

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

void fn( )

{

MyStruct source , destination ;

destination = source ;

}

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

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

void fn( MyClass& mc )

{

MyClass newMC( mc ) ; /* Здесь используется конструктор копирования */

MyClass newerMC = mc ; /* Менее очевидно, что здесь также используется конструктор копирования */

MyClass newestMC ; /* Создание объекта по умолчанию */

newestMC = mc ; /* Присвоение */

}

_________________

272 стр. Часть 5. Полезные особенности

Создание newMC следует стандартному шаблону создания нового объекта как зеркального отображения существующего с использованием копирующего конструктора MyClass( MyClass& ). Несколько менее очевидно, что объект newerMC также создаётся при помощи конструктора копирования. Запись MyClass а = b — всего лишь другой способ записи MyClass a( b ). То, что в первом варианте записи имеется символ "=", не приводит к вызову оператора присвоения. Однако в случае с объектом newestMC всё не совсем так. Сначала этот объект создаётся с использованием конструктора по умолчанию, а затем перезаписывается объектом mc с помощью оператора присвоения.

Подобно конструктору копирования, оператор присвоения должен быть переопределён, если мелкое копирование приводит к некорректным результатам ( см. материал, представленный в главе 18, "Копирующий конструктор" ). Простейшее правило: если у класса есть пользовательский конструктор копирования, то переопределите для него и оператор присвоения.

«Главное правило заключается в следующем: конструктор копирования используется при создании нового объекта, а оператор присвоения — если объект слева от символа присвоения уже существует.»

[Советы]

►Переопределение оператора присвоения...273

Следующая программа демонстрирует переопределение оператора присвоения. В программе также представлен конструктор копирования — просто для сравнения.

//

/* DemoAssignmentOperator — демонстрация оператора */

/*          присвоения для пользовательского класса */

//

#include <cstdio>

#include <cstdlib>

#include <iostream>

#include <strings.h>

using namespace std ;

/* Name — класс для демонстрации присвоения и конструктора копирования */

class Name

{

public :

Name( char *pszN = 0 )

{

copyName( pszN , " " ) ;

}

Name( Name& s )

{

cout << "Вызов конструктора копирования" << endl ;

copyName( s.pszName , " ( copy ) " ) ;

}

~Name( )

{

deleteName( ) ;

}

/* Оператор присвоения */

Name& operator=( Name& s )

{

cout << "Выполнение присвоения" << endl ;

_________________

273 стр. Глава 23. Оператор присвоения

/* Удаляем выделенную память... */

deleteName( ) ;

/* ...перед заменой её новым блоком */

copyName( s.pszName , " ( replaced ) " ) ;

/* Возвращаем ссылку  на существующий объект */

return *this ;

}

/* Очень простая функция доступа */

char* out( ) { return pszName ; }

protected :

void copyName( char* pszN , char* pszAdd ) ;

void deleteName( ) ;

char *pszName ;

} ;

/* copyName( ) — Выделение памяти из кучи и сохранение строк в ней */

void Name::copyName( char* pszN , char* pszAdd )

{

pszName = 0 ;

if ( pszN )

{

pszName = new char[ strlen( pszN ) +

strlen( pszAdd ) + 1 ] ;

strcpy( pszName , pszN ) ;

strcat( pszName , pszAdd ) ;

}

}

/* deleteName( ) — возврат памяти в куче */

void Name::deleteName( )

{

if ( pszName )

{

delete pszName ;

pszName = 0 ;

}

}

int main( int nNumberofArgs , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */

/* Создание двух объектов */

Name n1( "Claudette" ) ;

Name n2( "Greg" ) ;

cout << "n1 ( " << n1.out( ) << " ) и "

<<"n2 ( " << n2.out( ) << " ) — "

<< "вновь созданные объекты"

<< endl ;

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

cout << "Name n3( n1 ) ;" << endl ;

Name n3( n1 ) ;

cout << "n3 ( " << n3.out( ) << " ) — копия n1" << endl ;

_________________

274 стр. Часть 5. Полезные особенности

/* Создание нового объекта с использованием формата с "=" для обращения к конструктору копирования */

cout << "Name n4 = n1 ;" << endl ;

Name n4 = n1 ;

cout << "n4 ( " << n4.out( ) << " ) — ещё одна копия n1"

<< endl ;

/* Перезапись n2 объектом n1 */

cout << "n2 = n1" << endl ;

n2 = n1 ;

cout << "n1 ( " << n1.out( ) << " ) присвоен объекту "

<< "n2 ( " << n2.out( ) << " )" << endl ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

Класс Name содержит указатель на имя человека, которое записывается в блок памяти, выделяемый из кучи. Конструктор и деструктор класса Name аналогичны представленным в главах 17, "Аргументация конструирования", и 18, "Копирующий конструктор". Конструктор Name( char* ) копирует переданное ему имя в член pszName. Этот конструктор служит также в роли конструктора по умолчанию. Конструктор копирования Name( Name& ) копирует имя переданного объекта при помощи функции-члена copyName( ). Деструктор освобождает блок памяти при помощи вызова deleteName( ).

Оператор присвоения operator=( ) является методом класса. Он выглядит как деструктор, за которым тут же следует конструктор копирования, что представляет собой вполне типичную ситуацию. Рассмотрим присвоение n2 = n1. Объект n2 уже имеет связанное с ним имя "Greg". В процессе присвоения память, выделенная для этого имени, освобождается при помощи вызова deleteName( ), так же, как это делается в деструкторе. Затем оператор присвоения вызывает copyName( ) для копирования новой информации в объект, подобно тому, как это делается в конструкторе копирования.

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

Есть ещё пара деталей, о которых следует упомянуть. Во-первых, возвращаемый оператором присвоения тип — Name&. Выражения, включающие оператор присвоения, имеют тип и значение, которые совпадают с типом и значением левого аргумента после присвоения. В следующем примере значение operator=( ) равно 2.0 , а его тип — double.

double d1 , d2 ;

void fn( double )

d1 = 2.0 ; /* Значение этого выражения равно 2.0 */

Это позволяет программисту написать следующее:

d2 = d1 = 2.0 ;

fn( d2 = 3.0 ) ; /* Выполняет присвоение и передаёт полученное значение функции fn( ) */

Значение присвоения d1 = 2.0, равное 2.0, и его тип double передаются для присвоения d2. Во втором примере значение присвоения d2 = 3.0 передаётся функции fn( ).

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

_________________

275 стр. Глава 23. Оператор присвоения

►Защита от копирования...276

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

class Name

{

/* ...всё, как и раньше... */

protected :

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

Name( Name& ){ } ;

/* Оператор присвоения */

Name& operator=( Name& s ) { return *this ; }

}

Присвоения наподобие приведённого далее ( при таком определении ) будут запрещены[ 18 ].

void fn( Name& n )

{

Name newN ;

newN = n ; /* Ошибка  компиляции — функция не */

/* имеет права доступа к operator=( ) */

}

Такая защита от копирования спасает вас от перегрузки оператора присвоения, но при этом снижает гибкость вашего класса.

«Если ваш класс использует какие-либо ресурсы, например, память из кучи, вы обязаны либо разработать удовлетворительные оператор присвоения и конструктор копирования, либо сделать их защищёнными для предотвращения их использования.»

[Советы]

______________

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

_________________

276 стр. Часть 5. Полезные особенности

Глава 24. ИСПОЛЬЗОВАНИЕ ПОТОКОВ ВВОДА-ВЫВОДА...277

ОГЛАВЛЕНИЕ

В этой главе...

►Как работают потоки ввода-вывода 277

►Знакомство с подклассами fstream  278

►Прямое чтение из потока 282

►Что такое endl 284

►Подклассы strstream 285

►Работа с манипуляторами 287

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

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

►Как работают потоки ввода-вывода...277

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

/* Операторы для ввода: */

istream& operator>>( istream&  source , char* pDest ) ;

istream& operator>>( istream& source , int& dest ) ;

istream& operator>>( istream& source , char& dest ) ;

/* ...и так далее... */

/* Операторы для вывода: */

istream& operator<<( ostream& dest , char* pSource ) ;

istream& operator<<( ostream& dest , int& source ) ;

stream& operator<<( ostream& dest , char& source ) ;

/* ...и так далее... */

Оператор operator>>( ) называется оператором извлечения из потока, а operator<<( ) — оператором вставки в поток. Класс istream является базовым для ввода информации из файла или устройства ввода типа клавиатуры. При запуске программы на выполнение С++ открывает объект cin класса istream. Аналогично, ostream представляет собой базовый класс для файлового вывода, a cout — объект класса ostream по умолчанию.

_________________

277 стр. Глава 24. Использование потоков ввода-вывода

«Рассмотрим, что получится, если написать следующий код ( имеющийся на прилагаемом компакт-диске ).»

[Диск]

/* DefaultStreamOutput */

#include <iostream>

#include <cstdlib >

using namespace std ;

void fn( ostream& out )

{

out << "Меня зовут Стефан\n" ;

}

int main( int nNumberofArgs , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */

fn( cout ) ;

system( "PAUSE" ) ; return 0 ;

}

Программа передаёт функции fn( ) поток cout. Функция использует оператор operator<<( ). Сначала С++ определит, что левый аргумент имеет тип ostream, а правый — тип char*. Вооружённый этими знаниями, он найдёт прототип функции operator<<( ostream& , char* ) в заголовочном файле iostream. Затем С++ вызовет функцию вставки в поток для char*, передавая ей строку "Меня зовут Стефан\n" и объект cout в качестве аргументов. Другими словами, он вызовет функцию operator<<( cout , "Меня зовут Стефан\n" ). Функция для вставки char* в поток, которая является частью стандартной библиотеки С++, выполнит необходимый вывод.

Но откуда компилятору известно, что cout является объектом класса ostream? Этот и ещё несколько глобальных объектов объявлены в файле iostream.h ( их список приведён в табл. 24.1 ). Эти объекты автоматически конструируются при запуске программы, до того как main( ) получает управление.

Таблица 24.1. Стандартные потоки ввода-вывода

_________________

Объект — Класс — Назначение

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

cin — istream — Стандартный ввод

cout — ostream — Стандартный вывод

cerr — ostream — Стандартный небуферизованный вывод сообщений об ошибках

clog — ostream — Стандартный буферизованный вывод сообщений об ошибках

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

►Знакомство с подклассами fstream...278

Подклассы ofstream, ifstream и fstream объявлены в заголовочном файле fstream.h и обеспечивают потоки ввода-вывода в дисковые файлы. Эти три класса предоставляют множество функций для управления вводом и выводом, многие из которых наследуются от ostream и istream. Полный список этих функций вы можете найти в документации к компилятору, а здесь я приведу только несколько из них, чтобы вы могли с чего-то начать.

_________________

278 стр. Часть 5. Полезные особенности

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

ofstream::ofstream( char* pFileName ,

int mode = ios::out ,

int prot = filebuff::openprot ) ;

Первый аргумент этого конструктора — указатель на имя открываемого файла. Второй и третий аргументы определяют, как именно должен быть открыт файл. Корректные значения аргумента mode приведены в табл. 24.2, a prot — в табл. 24.3. Эти значения являются битовыми полями, к которым применяется оператор побитового ИЛИ ( классы ios и filebuff — родительские по отношению к ostream ).

«Выражение ios::out представляет статический член-данные класса ios.»

[Советы]

Таблица 24.2. Значения аргумента mode в конструкторе класса ofstream

_________________

Флаг — Назначение

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

ios::app — Дописывать в конец файла. Вернуть ошибку, если файл не существует

ios::ate — Дописывать в конец файла, если он существует

ios::in — Открыть файл для ввода ( подразумевается для istream )

ios::out — Открыть файл для вывода ( подразумевается для ostream )

ios::trunc — Обрезать файл до нулевой длины, если он существует ( используется по умолчанию )

ios::nocreate — Если файла не существует, вернуть сообщение об ошибке

ios::noreplace — Если файл существует, вернуть сообщение об ошибке

ios::binary — Открыть файл в бинарном режиме ( альтернатива текстовому режиму )

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

Таблица 24.3. Значения аргумента prot в конструкторе класса ofstream

_________________

Флаг — Назначение

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

filebuf::openprot — Режим совместного чтения и записи

filebuf::sh_none — Исключительный режим без совместного доступа

filebuf::sh_read — Режим совместного чтения

filebuf::sh_write — Режим совместной записи

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

Приведённая ниже программа открывает файл MyName.txt, а затем записывает в него некоторую важную информацию.

/* StreamOutput — простой вывод в файл */

#include <fstream>

using namespace std ;

int main( int nNumberofArgs , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */

ofstream my( "MyName.txt" ) ;

my << "С++ для чайников — очень хорошая книга"

<< endl ;

system( "PAUSE" ) ; return 0 ;

}

_________________

279 стр. Глава 24. Использование потоков ввода-вывода

Конструктор ofstream::ofstream( char* ) получает только имя, а потому использует для режима открытия файла значения по умолчанию. Если файл MyName.txt уже существует, он урезается; в противном случае создаётся новый файл MyName.txt. Кроме того, файл открывается в режиме совместного чтения и записи.

Второй конструктор, ofstream::ofstream( char* , int ), позволяет программисту указывать другие режимы ввода-вывода. Например, если бы я захотел открыть файл в бинарном режиме и произвести запись в конец этого файла ( если он уже существует ), я мог бы создать объект класса ofstream так, как это показано ниже ( напомню, что в бинарном режиме при выводе не выполняется преобразование символа новой строки \n в пару символов перевода каретки и новой строки \r\n, так же как при вводе не происходит обратного преобразования ).

void fn( )

{

/* Откроем бинарный файл BINFILE для записи; если он  существует, дописываем информацию в конец файла */

ofstream bfile( "BINFILE", ios::binary | ios::ate ) ;

/* ...продолжение программы... */

}

Потоковые объекты хранят информацию о состоянии процесса ввода-вывода. Функция-член bad( ) возвращает TRUE, если при работе с файловым объектом произошло что-то "плохое". Сюда входят такие неприятности, как невозможность открыть файл, нарушение внутренней структуры и т.п. Функция fail( ) указывает, что либо произошла ошибка, либо последнее чтение было неудачным. Функция good( ) возвращает TRUE, если и bad( ), и fail( ) возвращают FALSE. Функция clear( ) используется для сброса флага ошибки. Вот как выглядит добавление простейшей обработки ошибок к рассмотренной нами программе.

/* StreamOutputWithErrorChecking —  простой вывод в файл */

#include <fstream>

#include <iostream>

using namespace std ;

int main( int nNumberofArgs , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */

const static char fileName[ ] = "MyName.txt" ;

ofstream my( fileName ) ;

if ( my.bad( ) ) /* Открыть не удалось... */

{

cerr << "Ошибка открытия файла "

<< fileName

<< endl ;

return 0 ; /* ...вывод сообщения и завершение работы */

}

my << "С++ для чайников — очень хорошая книга"

<< endl ;

if ( my.bad( ) )

{

cerr << "Ошибка записи в файл "

<< fileName

<< endl ;

}

system( "PAUSE" ) ; return 0 ;

}

_________________

280 стр. Часть 5. Полезные особенности

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

[Советы]

Деструктор класса ofstream автоматически закрывает файл. В предыдущем примере файл был закрыт при выходе из функции.

Класс ifstream работает для ввода почти так же, как ofstream для вывода, что и демонстрирует приведённый ниже пример.

/* StreamInput — ВВОД ДАННЫХ С ИСПОЛЬЗОВАНИЕМ fstream */

#include <fstream>

#include <cstdlib>

#include <iostream>

using namespace std ;

ifstream* openFile( )

{

ifstream* pFileStream = 0 ;

for ( ; ; )

{

/* Открытие файла, указанного пользователем */

char fileName[ 80 ] ;

cout << "Введите имя файла с целыми числами"

<< endl ;

cin >> fileName ;

/* Открываем файл для чтения; не создавать файл, если его не существует */

pFileStream = new ifstream( fileName ) ;

if ( pFileStream -> good( ) )

{

break ;

}

cerr << "Невозможно открыть " << fileName << endl ;

delete pFileStream ;

}

return pFileStream ;

}

int main( int nNumberofArgs , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */

/* Получение файлового потока */

ifstream* pFileStream = openFile( ) ;

/* Остановиться по достижению конца файла */

while ( !pFileStream -> eof( ) )

{

/* Чтение значения */

int nValue = 0 ;

( *pFileStream ) >> nValue ;

/* Останов при ошибке чтения ( например, считывается не целое число, или считан символ новой строки, после которого ничего нет */

if ( pFileStream -> fail( ) )

{

break ;

}

/* Вывод считанного значения */

cout << nValue << endl ;

}

delete pFileStream ;

system( "PAUSE" ) ; return 0 ;

}

_________________

281 стр. Глава 24. Использование потоков ввода-вывода

Функция openFile( ) запрашивает у пользователя имя открываемого файла и создаёт поток с этим именем. Создание объекта ifstream автоматически открывает файл для ввода. Если файл открыт корректно, функция возвращает указатель на объект ifstream, который используется для чтения. В противном случае объект удаляется и повторяется попытка открыть файл. Единственный способ выйти из цикла — ввести правильное имя файла или завершить выполнение программы.

«Не забывайте о необходимости удаления pFileStream, если вы не смогли открыть файл. Это позволит избежать утечек памяти.»

[Советы]

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

«Ещё раз напомню, что при чтении из потока в состоянии ошибки ничего считано не будет. Более того, буфер останется неизменным, так что программа может прийти к ошибочному выводу, что прочитано такое же значение, как и перед этим. Кстати, при наличии ошибки функция eof( ) никогда не вернёт true.»

[Советы]

Вывод этой программы имеет следующий вид.

Введите имя файла с целыми числами

testfile

Невозможно открыть testfile

Введите имя файла с целыми числами

integers.txt

123

456

234

654

4363

48923

78237

Press any key to continue...

►Прямое чтение из потока...282

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

_________________

282 стр. Часть 5. Полезные особенности

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

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

/* FileInput — чтение блока данных из файла */

#include <fstream>

#include <cstdlib>

#include <iostream>

using namespace std ;

ifstream* openFile( istream& input )

{

for ( ; ; )

{

/* Открытие определённого пользователем файла */

char fileName[ 80 ] ;

cout << "    Введите имя файла" << endl ;

/* Чтение вводимого пользователем имени ( не более 80 символов, что обеспечивает невозможность переполнения буфера ) */

input.getline( fileName , 80 ) ;

/* Открываем файл для чтения; если его нет — заново его не создаём */

ifstream* pFileStream = new ifstream( fileName ) ;

if ( pFileStream -> good( ) )

{

return pFileStream ;

}

cerr << "Невозможно найти файл " << fileName << endl ;

}

return 0 ;

}

int main( int nNumberofArgs , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */

/* Получаем поток */

ifstream* pFileStream = openFile( cin ) ;

/* Читаем его блоками по 80 байт */

char buffer[ 80 ] ;

while ( !pFileStream -> eof( ) && pFileStream -> good( ) )

{

/* Чтение блоками; gcount( ) возвращает количество реально считанных байт */

pFileStream -> read( buffer , 80 ) ;

int noBytes = pFileStream -> gcount( ) ;

/* Работа с блоком */

for ( int i = 0 ; i < noBytes ; i++ )

{

cout << buffer[ i ] ;

}

}

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

_________________

283 стр. Глава 24. Использование потоков ввода-вывода

В данной программе сначала вызывается функция openFile( ), открывающая файл, имя которого вводится пользователем. Здесь есть два интересных момента. Во-первых, функция читает объект istream так же, как ранее — cin ( функция main( ) передаёт функции openFile( ) поток cin в качестве аргумента ). Однако данная функция может использовать произвольный поток istream без каких-либо модификаций.

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

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

[Советы]

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

Введите имя файла

integers.txt

123 456 234 654

4363 48923 78237 dhbj

dnbsd

93276823 4329

Press any key to continue...

►Что такое endl...284

Большинство программ в данной книге завершают вывод в поток вставкой объекта endl. Однако некоторые программы включают в выводимый текст символ \n. В чём тут дело?

Символ \n — символ новой строки. Так, выражение соut<<"Первая строка\nВторая строка" выведет две строки. При вставке объекта endl также произойдёт вывод символа новой строки, но при этом выполняется ещё одно действие.

Диски — медленные устройства, и чтобы вывод на диск меньше замедлял работу программы, fstream накапливает выводимые данные во внутреннем буфере. Класс выводит буфер на диск по его заполнении. Вставка же объекта endl заставляет сбросить на диск всё, что есть в буфере, независимо от его заполненности. Сбросить буфер без вывода символа новой строки можно при помощи явного вызова функции-члена flush( ).

_________________

284 стр. Часть 5. Полезные особенности

►Подклассы strstream...285

Потоковые классы позволяют программисту разбивать входные данные на числа и массивы символов. Так называемые "строковые потоки" позволяют использовать операции, определённые для файлов в классах fstream, для строк в памяти. Соответствующие классы istringstream и ostringstream определены в заголовочном файле sstream.

«В старых версиях С++ эти классы назывались istrstream и ostrstream и были определены в заголовочном файле strstream.»

[Советы]

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

/* StringStream — чтение и разбор содержимого файла */

#include <fstream>

#include <sstream>

#include <iostream>

using namespace std ;

/* parseAccountInfo — чтение переданного */

/*                    буфера как если бы */

/*                       это был файл. */

/*              Формат: имя, счёт, баланс. */

/*                   При корректной работе */

/*                      возвращает true. */

bool parseString( char* pString ,

char* pName ,

int arraySize ,

long& accountNum ,

double& balance )

{

/* Связывает объект istringstream с входной строкой */

istringstream inp( pString ) ;

/* Чтение до разделяющей запятой */

inp.getline( pName , arraySize , ',' ) ;

// Номер счёта

inp >> accountNum ;

// и его баланс

inp >> balance ;

/* Возврат состояния ошибки */

return !inp.fail( ) ;

}

int main( int nNumberofArgs , char* pszArgs[ ] )

{

setlocale ( LC_ALL ,".1251" ) ; /* печать русских текстов */

/* Файловый поток */

ifstream* pFileStream = new ifstream( "Accounts.txt" ) ;

if ( !pFileStream -> good( ) )

{

cout << "Невозможно открыть Accounts.txt" << endl ;

return 0 ;

}

/* Считываем строку файла, разбираем и выводим результат */

_________________

285 стр. Глава 24. Использование потоков ввода-вывода

for ( ; ; )

{

/* Добавляем разделитель */

cout << "=================" << endl ;

/* Читаем в буфер */

char buffer[ 256 ] ;

pFileStream -> getline( buffer , 256 ) ;

if ( pFileStream -> fail( ) )

{

break ;

}

/* Разбираем ввод на поля */

char name[ 80 ] ;

long accountNum ;

double balance ;

bool result = parseString( buffer , name , 80 ,

accountNum , balance ) ;

/* Вывод результата */

cout << buffer << "\n" ;

if ( result == false )

{

cout << "Ошибка разбора строки\n" ;

continue ;

}

cout << "Имя = " << name << ","

<< "Счёт = " << accountNum << ", "

<< "Баланс = " << balance << endl ;

/* Выводим поля в другом порядке ( вставка ends гарантирует нуль-завершённость буфера ) */

ostringstream out ;

out << name << ", "

<< balance << " "

<< accountNum << ends ;

/* Вывод результата. Класс istringstream работает и с классом string, но пока что мы не будем использовать эту возможность */

string oString = out.str( ) ;

cout << oString << "\n" << endl ;

}

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

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

Имя, номер счёта, баланс, \n

В предположении, что файл открыт успешно, программа входит в цикл, считывающий файл построчно до его полного прочтения. Строки считываются при помощи функции-члена getline( ), который читает их до символа новой строки. Затем считанная строка передаётся функции parseString( ).

_________________

286 стр. Часть 5. Полезные особенности

Функция parseString( ) связывает с символьной строкой объект istringstream. Программа считывает символы из строки до достижения символа '.' ( или конца строки ) при помощи функции-члена getline( ), после чего используются обычные операторы извлечения из потока для чтения номера счёта и баланса. Чтение выполнено успешно, если inp.fail( ) возвращает false.

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

================

Chester, 12345 56.60

Имя = Chester,Счёт = 12345, Баланс = 56.6

Chester, 56.6 12345

================

Arthur, 34567 67.50

Имя = Arthur,Счёт = 34567, Баланс = 67.5

Arthur, 67.5 34567

================

Trudie, 56x78 78.90

Ошибка разбора строки

================

Valerie, 78901 89.10

Имя = Valerie,Счёт = 78901, Баланс = 89.1

Valerie, 89.1 78901

===============

Press any key to continue...

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

►Работа с манипуляторами...287

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

Например, мне совсем не нравится, когда общая сумма в моей любимой финансовой программе выводится как 249.600006 вместо ожидаемого 249.6 ( а ещё лучше — 249.60 ). Необходимо каким-то образом указать программе количество выводимых цифр после десятичной точки. И такой способ есть; более того, в С++ он не единственный.

«В зависимости от установок по умолчанию вашего компилятора, вы можете увидеть на экране 249.6. Однако хотелось бы добиться того, чтобы выводилось именно 249.60.»

[Атас!]

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

#include <iostream>

void fn( float interest , float dollarAmount )

{

cout << "Сумма в долларах = " ;

cout.precision( 2 ) ;

cout << dollarAmount ;

cout.precision( 4 ) ;

cout << interest

<< "\n" ;

}

_________________

287 стр. Глава 24. Использование потоков ввода-вывода

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

Второй путь связан с использованием так называемых манипуляторов. ( Звучит страшновато, не так ли? ) Манипуляторы — это объекты, определённые в заголовочном файле iomanip.h, которые приводят к тому же эффекту, что и описанные выше функции-члены ( чтобы иметь возможность пользоваться манипуляторами, вы должны не забыть включить iomanip.h в программу ). Единственное преимущество манипуляторов в том, что программа может включать их прямо в поток, не прибегая к вызову отдельной функции.

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

#include <iostream>

#include <iomanip>

void fn( float interest , float dollarAmount )

{

cout << "Сумма в долларах = " ;

<< setprecision( 2 ) << dollarAmount

<< setprecision( 4 ) << interest

<< "\n" ;

}

Наиболее распространённые манипуляторы и их назначение приведены в табл. 24.4.

Таблица 24.4. Основные манипуляторы и функции управления форматом потока

_________________

Манипулятор — Функция-член — Описание

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

dec — flags( 10 ) — Перейти в десятичную систему счисления

hex — flags( 16 ) — Перейти в шестнадцатеричную систему счисления

oct — flags( 8 ) — Перейти в восьмеричную систему счисления

setfill( с ) — fill( c ) Установить символ заполнения с

setprecision( с ) — precision( с ) — Установить количество отображаемых знаков после запятой в с

setw( n ) — width( n ) — Установить ширину поля равной n символов*

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

_________________

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

«Внимательно следите за параметром ширины поля ( функция width( n ) либо манипулятор setw( n ) ). Большинство параметров сохраняют своё значение до тех пор, пока оно не будет изменено новым вызовом, однако для параметра ширины поля это не так. Этот параметр возвращается к значению по умолчанию, как только будет выполнен следующий вывод в поток. Например, приведённый ниже фрагмент кода не выведет два целочисленных значения длиной в 8 символов.»

[Атас!]

_________________

288 стр. Часть 5. Полезные особенности

#include <iostream>

#include <iomanip>

void fn( )

{

cout << setw( 8 ) /* ширина поля равна 8... */

<< 10      /* ...для 10 , но... */

<< 20 /* для 20 равна значению по умолчанию */

<< "\n" ;

}

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

#include <iostream>

#include <iomanip>

void fn( )

{

cout << setw( 8 ) /* установить ширину... */

<< 10

<< setw( 8 )

<< 20 /* ...обновить её */

<< "\n" ;

}

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

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

#include <iostream>

void fn( float value )

{

int previousPrecision ;

/* Вы можете узнать текущую точность так: */

previousPrecision = cout.precision( ) ;

/* Можно также сохранить старое значение, одновременно изменяя его на новое */

previousPrecision = cout.precision( 2 ) ;

cout << value ;

/* Восстановим предыдущее значение */

cout.precision( previousPrecision ) ;

}

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

_________________

289 стр. Глава 24. Использование потоков ввода-вывода

Глава 25. ОБРАБОТКА ОШИБОК И ИСКЛЮЧЕНИЯ...290

ОГЛАВЛЕНИЕ

В этой главе...

►Зачем нужен новый механизм обработки ошибок 291

►Механизм исключительных ситуаций 293

►Так что же мы будем бросать? 295

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

Механизм исключительных ситуаций базируется на ключевых словах try ( попытаться, пробовать, попытка — [trai] — [трай] ), throw ( бросить, бросание, бросок —   [θrou] — [сроу] ) и catch (  поймать, схватить, ловить — [kætʃ] — [кэчь] ). В общих чертах этот механизм работает так: функция пытается ( пробует — try ) выполнить фрагмент кода. Если в коде содержится ошибка, функция бросает ( генерирует — throw ) сообщение об ошибке, которое должна поймать ( перехватить — catch ) вызывающая функция. Это продемонстрировано в приведённой ниже программе.

//

/* FactorialException — демонстрация исключений */

/*                  при использовании факториала */

//

#include <cstdio>

#include <cstdlib>

#include <iostream>

#include <string>

using namespace std ;

/* factorial — вычисление факториала */

int factorial( int n )

{

/* Функция не обрабатывает отрицательные  значения аргумента */

if ( n < 0 )

{

throw string( "Аргумент отрицателен" ) ;

}

/* Вычисляем факториал */

int accum = 1 ;

while ( n > 0 )

{

accum *= n ;

_________________

290 стр. Часть 5. Полезные особенности

n-- ;

}

return accum ;

}

int main( int nNumberofArgs , char* pszArgs[ ] )

{

/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */

setlocale ( LC_ALL , ".1251" ) ;

try

{

/* Здесь всё в порядке */

cout << "3! = " << factorial( 3 ) << endl ;

/* Здесь генерируется исключение */

cout << "-1!= " << factorial( -1 ) << endl ;

/* Этот код так и  остаётся не выполнен */

cout << "Factorial of 5 is " << factorial( 5 )

<< endl ;

}

/* Обработка исключения */

catch( string error )

{

cout << "Ошибка: " << error << endl ;

}

catch ( ... )

{

cout << "Неизвестное исключение" << endl ;

}

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

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

►Зачем, нужен новый механизм обработки ошибок...291

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

_________________

291 стр. Глава 25. Обработка ошибок и исключения

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

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

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

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

errRtn = someFunc( ) ;

if ( errRtn )

{

errorOut( "Ошибка при вызове someFn( )" ) ;

return MY_ERROR_1

}

errRtn = someOtherFunc( ) ;

if ( errRtn )

{

errorOut( "Ошибка при вызове someOtherFn( )" ) ;

return MY_ERROR_1

}

Такой механизм имеет ряд недостатков.

■■■

■ Изобилует повторениями.

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

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

■■■

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

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

_________________

292 стр. Часть 5. Полезные особенности

►Механизм исключительных ситуаций...293

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

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

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

Когда необходимый блок try найден, программа ищет первый блок catch ( который должен находиться сразу за закрывающей скобкой блока try ). Если тип сгенерированного объекта совпадает с типом аргумента, указанным в блоке catch, управление передаётся этому блоку; если же нет, проверяется следующий блок catch. Если в результате подходящий блок не найден, программа продолжает поиск уровнем выше, пока не будет обнаружен необходимый блок catch. Если искомый блок не обнаружен, программа аварийно завершается. Рассмотрим приведённый ниже пример.

/* CascadingException — при компиляции данная программа */

/*                   может вызвать предупреждения о */

/*                  том, что переменные f, i и pMsg */

/*                   не используются */

#include <cstdio>

#include <cstdlib>

#include <iostream>

#include <string>

using namespace std ;

class Obj

{

public :

Obj( char c )

{

label = c ;

cout << "Конструирование объекта " << label << endl ;

}

~Obj ( )

{

cout << "Деструкция объекта " << label << endl ;

}

protected :

char label ;

} ;

void f1( ) ;

void f2( ) ;

int f3( )

{

Obj a( 'a' ) ;

try

{

_________________

293 стр. Глава 25. Обработка ошибок и исключения

Obj b( 'b' ) ;

f1( ) ;

}

catch( float f )

{

cout << "Перехват float" << endl ;

}

catch( int i )

{

cout << "Перехват int" << endl ;

}

catch ( ... )

{

cout << string( "Обобщённое исключение" ) << endl ;

}

return 0;

}

int main( int nNumberofArgs , char* pszArgs[ ] )

{

/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */

setlocale ( LC_ALL , ".1251" ) ;

f3( ) ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

void f1( )

{

try

{

Obj c( 'c' ) ;

f2( ) ;

}

catch( string msg )

{

cout << "Перехват строки" << endl ;

}

}

void f2( )

{

Obj d( 'd' ) ;

throw 10 ;

}

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

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

Конструирование объекта b

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

Конструирование объекта d

Деструкция объекта d

Деструкция объекта с

Деструкция объекта b

Перехват int

Деструкция объекта а

Press any key to continue...

_________________

294 стр. Часть 5. Полезные особенности

Как видите, прежде чем в функции f2( ) происходит исключение int, конструируются четыре объекта — а, b, с и d. Поскольку в f2( ) блок try не определён, С++ разворачивает стек вызовов функций, что приводит к ликвидации объекта d при разворачивании стека f2( ). В функции f1( ) определён блок try, но его блок catch воспринимает только char*, что не совпадает с брошенным объектом int. Поэтому С++ продолжает просмотр, что приводит к разворачиванию стека функции f1( ) ( при этом ликвидируется объект с ).

В функции f3( ) С++ находит ещё один блок try. Выход из этого блока приводит к выходу из области видимости объекта и b. Первый за блоком try блок catch принимает float, что вновь не совпадает с нашим int, поэтому пропускается и этот блок. Однако следующий блок catch наконец-то воспринимает int, и управление переходит к нему. Последний блок catch, который воспринимает любой объект, пропускается, поскольку необходимый блок catch уже найден и исключение обработано.

►Так что же мы будем бросать?...295

За ключевым словом throw следует выражение, которое создаёт объект некоторого типа. В приведённых здесь примерах мы генерировали переменные типа int, но на самом деле ключевое слово throw работает с любым типом объекта. Это значит, что вы можете "бросать" любое количество информации. Рассмотрим приведённое ниже определение класса.

//

/* CustomExceptionClass — демонстрация исключений */

/*                       при использовании факториала */

#include <cstdio>

#include <cstdlib>

#include <iostream>

#include <sstream>

using namespace std ;

/* Exception — обобщённый класс исключения */

class Exception

{

public :

Exception( char* pMsg , int n , char* pFile , int nLine )

: msg( pMsg ) , errorValue( n ) , file( pFile ) , lineNum( nLine )

{ }

virtual string display( )

{

ostringstream out ;

out << "Ошибка <" << msg

<< " - значение равно " << errorValue

<< ">\n" ;

out << "@" << file << "-" << lineNum << endl ;

return out.str( ) ;

}

protected :

/* Сообщение об ошибке */

string msg ;

int errorValue ;

/* Имя файла и номер строки, где произошла ошибка */

string file ;

_________________

295 стр. Глава 25. Обработка ошибок и исключения

int lineNum ;

} ;

/* factorial — вычисление факториала */

int factorial( int n )

{

/* Функция не обрабатывает отрицательные значения аргумента */

if ( n < 0 )

{

throw Exception( "Аргумент факториала отрицателен" ,

n , __FILE__ , __LINE__ ) ;

}

/* Вычисляем факториал */

int accum = 1 ;

while ( n > 0 )

{

accum *= n ;

n-- ;

}

return accum ;

}

int main( int nNumberofArgs , char* pszArgs[ ] )

{

/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */

setlocale  ( LC_ALL , ".1251" ) ;

try

{

/* Этот код работает корректно */

cout << "3! = " << factorial( 3 ) << endl ;

/* Здесь генерируется исключение */

cout << "Factorial of -1 is " << factorial( -1 ) << endl ;

/* Этот код остаётся невыполненным */

cout << "Factorial of 5 is " << factorial( 5 ) << endl ;

}

/* Перехват исключения */

catch( Exception e )

{

cout << "Ошибка: \n" << e.display( ) << endl ;

}

catch ( ... )

{

cout << "Неизвестное исключение" << endl ;

}

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

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

_________________

296 стр. Часть 5. Полезные особенности

«Встроенные макроопределения _FILE_ и _LINE_ представляют собой имя исходного файла и текущую строку в нём.»

[Советы]

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

3! = 6

Ошибка:

Ошибка <Аргумент факториала отрицателен — значение равно -1> @С:/Documents/Dial/CppDummy/CustomExceptionClass.срр-46

Press any key to continue...

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

class InvalidArgumentException : public Exception

{

public :

InvalidArgumentException( int arg , char*pFile , int nLine )

: Exception( "Некорректный аргумент" , pFile , nLine )

{

invArg = arg ;

}

virtual void display( ostream& out )

{

Exception::display( out ) ;

out << "Аргумент " << invArg << endl ;

}

protected :

int invArg ;

}

Вызывающая функция автоматически обработает новое исключение, поскольку InvalidArgumentException ЯВЛЯЕТСЯ Exception, а функция-член display( ) — полиморфна.

_________________

297 стр. Глава 25. Обработка ошибок и исключения

Глава 26. МНОЖЕСТВЕННОЕ НАСЛЕДОВАНИЕ...298

ОГЛАВЛЕНИЕ

В этой главе...

►Механизм множественного наследования 298

►Устранение неоднозначностей множественного наследования 300

►Виртуальное наследование 301

►Конструирование объектов 306

►Отрицательные стороны множественного наследования 306

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

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

►Механизм, множественного наследования...298

Чтобы увидеть множественное наследование в действии, я продолжу пример с диваном-кроватью. На рис. 26.1 приведена схема наследования дивана-кровати ( класс SleeperSofa ). Обратите внимание, что этот класс наследует свойства и от класса Bed ( Кровать ), и от класса Sofa ( Диван ), т.е. наследует свойства обоих классов.

Рис. 26.1. Иерархия классов дивана-кровати

_________________

298 стр. Часть 5. Полезные особенности

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

//

/* MultipleInheritance — класс, являющийся наследником */

/*                        нескольких базовых классов */

//

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

class Bed

{

public :

Bed( ) { }

void sleep( ) { cout << "Спим" << endl ; }

int weight ;

} ;

class Sofa

{

public :

Sofa( ) { }

void watchTV( ) { cout << "Смотрим телевизор" << endl ; }

int weight ;

} ;

/* SleeperSofa — диван-кровать */

class SleeperSofa : public Bed , public Sofa

{

public :

SleeperSofa( ) { }

void foldOut( ) { cout << "Раскладываем диван-кровать"

<< endl ; }

} ;

int main( int nNumberofArgs , char* pszArgs[ ] )

{

/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */

setlocale ( LC_ALL , ".1251" ) ;

SleeperSofa ss ;

/* Посмотрим телевизор на диване... */

ss.watchTV( ) ; /* Sofa::watchTV( ) */

/* ...разложим его в кровать... */

ss.foldOut( ) ; /* SleeperSofa::foldOut( ) */

/* ...и ляжем спать */

ss.sleep( ) ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

В этом примере класс SleeperSofa наследует оба класса — Bed и Sofa. Это видно из их наличия в объявлении класса SleeperSofa, который наследует все члены от обоих базовых классов. Таким образом, допустимы оба вызова — как ss.sleep( ), так и ss.watchTV( ). Вы можете использовать SleeperSofa и как Bed, и как Sofa. Кроме того, класс SleeperSofa имеет собственные члены, например foldOut( ). В результате мы получим следующий вывод программы:

Смотрим телевизор

Раскладываем диван-кровать

Спим

Press any key to continue...

_________________

299 стр. Глава 26. Множественное наследование

►Устранение неоднозначностей множественного наследования...300

Будучи весьма мощной возможностью языка, множественное наследование может стать в то же время и источником проблем. Одну из них можно увидеть уже в предыдущем примере. Обратите внимание, что оба класса — Bed и Sofa — содержат член weight ( вес ). Это логично, потому что они оба имеют некоторый вполне измеримый вес. Вопрос: какой именно член weight наследует класс SleeperSofa?

Ответ прост: оба. Класс SleeperSofa наследует отдельный член Bed::weight и отдельный член Sofa::weight. Поскольку они оба имеют одно и то же имя, обращения к weight теперь являются двузначными, если только не указывать явно, к какому именно weight мы намерены обратиться. Это демонстрирует следующий фрагмент кода:

#include <iostream>

void fn( )

{

SleeperSofa ss ;

cout << "Beс = "

<< ss.weight /* неправильно — какой именно вес? */

<< "\n" ;

}

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

#include <iostream>

void fn( )

{

SleeperSofa ss ;

cout << "Вес дивана = "

<< ss.Sofa::weight /*укажем, какой именно вес */

<< "\n" ;

}

Хотя такое решение и устраняет ошибку, указание имени базового класса во внешнем приложении нежелательно: ведь при этом информация о внутреннем устройстве класса должна присутствовать за его пределами. В нашем примере функция fn( ) должна располагать сведениями о том, что класс SleeperSofa наследуется от класса Sofa. Такие конфликты имён невозможны при одиночном наследовании, но служат постоянным источником неприятностей при наследовании множественном.

_________________

300 стр. Часть 5. Полезные особенности

►Виртуальное наследование...301

В случае класса SleeperSofa конфликт имён weight является, по сути, небольшим недоразумением. Ведь на самом деле диван-кровать не имеет отдельного веса как кровать, и отдельного веса как диван. Конфликт возник потому, что такая иерархия классов не вполне адекватно описывает реальный мир. Дело в том, что разложение на классы оказалось неполным.

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

Рис. 26.2. Выделение общих свойств кровати и дивана

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

[Диск]

//

/* MultipleInheritanceFactoring — класс, являющийся */

/*                                наследником нескольких */

/*                                базовых классов */

//

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

/* Furniture — фундаментальная концепция, обладающая весом */

class Furniture

{

_________________

301 стр. Глава 26. Множественное наследование

public :

Furniture( int w ) : weight( w ) { }

int weight ;

} ;

class Bed : public Furniture

{

public :

Bed( int weight ) : Furniture( weight ) { }

void sleep( ) { cout << "Спим" << endl ; }

} ;

class Sofa : public Furniture

{

public :

Sofa( int weight ) : Furniture( weight ) { }

void watchTV( ) { cout << "Смотрим телевизор" << endl ; }

} ;

/* SleeperSofa — диван-кровать */

class SleeperSofa : public Bed , public Sofa

{

public :

SleeperSofa( int weight ) : Sofa( weight ) , Bed( weight ) { }

void foldOut( ) { cout << "Раскладываем диван-кровать"

<< endl ; }

} ;

int main( int nNumberofArgs , char* pszArgs[ ] )

{

/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */

setlocale ( LC_ALL , ".1251" ) ;

SleeperSofa ss( 10 ) ;

/* Section 1 — неоднозначность: Furniture::Sofa или Furniture::Bed? */

/*

cout << "Beс = "

<< ss.weight

<< endl ;

*/

/* Section 2 — Один из способов устранения неоднозначности */

SleeperSofa* pSS = &ss ;

Sofa* pSofa = ( Sofa* )pSS ;

Furniture* pFurniture = ( Furniture* )pSofa ;

cout << "Beс = "

<< pFurniture -> weight

<< endl ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

_________________

302 стр. Часть 5. Полезные особенности

М-да... "Не говори "гоп", пока не переехал Чоп" — новая иерархия классов совершенно нас не спасает, weight остаётся неоднозначным. Попробуем привести ss к классу Furniture.

#include <iostream>

void fn( )

{

SleeperSofa ss ;

Furniture* pF ;

pF = ( Furniture* )&ss ;

cout << "Beс = "

<< pF  ->  weight

<< "\n" ;

} ;

Приведение ss к классу Furniture тоже ничего не даёт. Более того, я получил какое-то подозрительное сообщение о том, что приведение SleeperSofa* к классу Furniture* неоднозначно. Да что, в конце концов, творится?

На самом деле всё довольно просто. Класс SleeperSofa не наследуется напрямую от класса Furniture. Сначала Furniture наследуют классы Bed и Sofa, а уж потом SleeperSofa наследуется от этих классов. Класс SleeperSofa выглядит в памяти так, как показано на рис. 26.3.

Рис. 26.3. Расположение класса SleeperSofa в памяти

Как видите, SleeperSofa состоит из класса Bed, за которым в полном составе следует класс Sofa, а после него — уникальные члены класса SleeperSofa. Каждый из подобъектов класса SleeperSofa имеет свою собственную часть Furniture, поскольку они оба наследуются от этого класса. В результате объекты класса SleeperSofa содержат два объекта класса Furniture.

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

SleeperSofa содержит два объекта класса Furniture — явная бессмыслица! Необходимо, чтобы SleeperSofa наследовал только одну копию Furniture и чтобы Bed и Sofa имели к ней доступ. В С++ это достигается виртуальным наследованием, поскольку в этом случае используется ключевое слово virtual.

_________________

303 стр. Глава 26. Множественное наследование

Рис. 26.4. Результат попытки создания иерархии классов

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

[Советы]

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

//

/* VirtualInheritance — виртуальное */

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

/*                      классам Bed и Sofa использовать */

/*                      общий базовый класс */

//

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

/* Furniture — фундаментальная концепция, обладающая весом */

class Furniture

{

public :

Furniture( int w = 0 ) : weight( w ) { }

int weight ;

} ;

class Bed : virtual public Furniture

{

public :

Bed(  ) { }

void sleep( ) { cout << "Спим" << endl ; }

} ;

class Sofa : virtual public Furniture

_________________

304 стр. Часть 5. Полезные особенности

{

public :

Sofa(  )  { }

void watchTV( ) { cout << "Смотрим телевизор" << endl ; }

} ;

/* SleeperSofa — диван-кровать */

class SleeperSofa : public Bed , public Sofa

{

public :

SleeperSofa( int weight ) : Furniture( weight ) { }

void foldOut( ) { cout << "Раскладываем диван-кровать"

<< endl ; }

} ;

int main( int nNumberofArgs , char* pszArgs[ ] )

{

/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */

setlocale ( LC_ALL , ".1251" ) ;

SleeperSofa ss( 10 ) ;

/* Section 1 — неоднозначности больше нет, есть только один вес */

cout << "Вес = "

<< ss.weight

<< endl ;

/* Section 2 — Один из способов устранения неоднозначности */

SleeperSofa* pSS = &ss ;

Sofa* pSofa = ( Sofa* )pSS ;

Furniture* pFurniture = ( Furniture* )pSofa ;

cout << "Bec = "

<< pFurniture -> weight

<< endl ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

Обратите внимание на ключевое слово virtual, используемое при наследовании классов Bed и Sofa от класса Furniture. Оно означает примерно следующее: "Дайте-ка мне копию Furniture, но если она уже существует, то я использую именно её". В итоге класс SleeperSofa будет выглядеть, как показано на рис. 26.5.

Из этого рисунка видно, что класс SleeperSofa включает Furniture, а также части классов Bed и Sofa, не содержащие Furniture. Далее находятся уникальные для класса SleeperSofa члены ( элементы в памяти не обязательно будут располагаться именно в таком порядке, но в данном обсуждении это несущественно ).

Теперь обращение к члену weight в функции fn( ) не многозначно, поскольку SleeperSofa содержит только одну копию Furniture. Наследуя этот класс виртуально, мы получили желаемую структуру наследования ( см. рис. 26.2 ).

_________________

305 стр. Глава 26. Множественное наследование

Если виртуальное наследование так хорошо решает проблему неоднозначности, почему оно не является нормой? Во-первых, потому, что виртуально наследуемый класс обрабатывается иначе, чем обычный наследуемый базовый класс, что, в частности, выражается в повышенных накладных расходах. Во-вторых, у вас может появиться желание иметь две копии базового класса ( хотя это случается весьма редко ). Вспомним наши старые упражнения со студентами и преподавателями и допустим, что TeacherAssistant ( помощник преподавателя ) является одновременно и Teacher ( преподавателем ) и Student ( студентом ), которые, в свою очередь, являются подклассами Academician. Если университет даст помощнику преподавателя два идентификатора — и студента и преподавателя, то классу TeacherAssistant понадобятся две копии класса Academician.

Рис. 26.5. Расположение класса SleeperSofa в памяти при использовании виртуального наследования

►Конструирование объектов...306

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

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

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

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

23. И наконец, вызывается конструктор самого класса.

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

►Отрицательные стороны множественного наследования...306

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

_________________

306 стр. Часть 5. Полезные особенности

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

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

Только не поймите меня неправильно! Я не против множественного наследования. То, что Microsoft и другие компании эффективно используют множественное наследование в своих классах, доказывает, что так можно делать. Если бы этот механизм не стоил того, они бы его не использовали. Однако это отнюдь не значит, что множественное наследование имеет смысл применять, едва познакомившись с ним.

_________________

307 стр. Глава 26. Множественное наследование

Глава 27. ШАБЛОНЫ С++...308

ОГЛАВЛЕНИЕ

В этой главе...

►Обобщение функции в шаблон 309

►Шаблоны классов 311

►Зачем нужны шаблоны классов 314

►Советы по использованию шаблонов 316

Стандартная библиотека С++ предоставляет программисту множество различных функций. В ней представлены математические функции, функции для работы со временем и датами, функции ввода-вывода, системные функции. Во многих программах в этой книге использованы, например, функции для работы с нуль-завершёнными строками ( эти функции объявлены в заголовочном файле strings.h ). Типы аргументов большинства функций фиксированы. Так, например, оба аргумента функции strcpy( char* , const char* ) являются указателями на нуль-завершённые строки — любые другие типы аргументов для данной функции просто лишены смысла.

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

Таблица 27.1. Возможные варианты функции maximum( )

_________________

Имя функции — Выполняемые действия

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

maximum( int , int ) — Возвращает большее из двух целых чисел

maximum( unsigned int , unsigned int ) — Возвращает большее из двух беззнаковых целых чисел

maximum( double , double ) — Возвращает большее из двух чисел с плавающей точкой

maximum( char , char ) — Возвращает символ, находящийся далее в алфавитном порядке

¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯

Я бы хотел реализовать функцию maximum( ) для всех четырёх случаев. Само собой, С++ может привести все эти типы к типу double, т.е. мне достаточно разработать функцию maximum ( double , double ), которая удовлетворит все мои потребности. Так ли это? Рассмотрим следующий код.

/* Прототип функции maximum */

double maximum( double , double ) ;

/* Пользовательская функция */

void fn ( int nArg1 , int nArg2 )

{

int nLarger = ( int )maximum( ( double )nArg1 ,

( double )nArg2 ) ;

/* ... прочие действия ... */

}

_________________

308 стр. Часть 5. Полезные особенности

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

Конечно, функцию maximum( ) можно просто перегрузить.

double maximum( double d1 , double d2 )

{

if ( d > d2 ) return d1 ;

return d2 ;

}

int maximum( int n1 , int n2 )

{

if ( n1 > n2 ) return n1 ;

return n2 ;

}

char maximum( char c1 , char c2 )

{

if ( c1 > c2 ) return c1 ;

return c2 ;

}

/* Определения функции для других типов */

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

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

►Обобщение функции в шаблон...309

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

/* MaxTemplate — шаблон функции maximum( ), возвращающей */

/*             наибольшее значение из двух аргументов */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

template < class T >

T maximum( T t1 , T t2 )

{

if ( t1 > t2 )

{

return t1 ;

}

return t2 ;

} ;

int main( int argc , char* pArgs[ ] )

{

/* печать кириллицы, если Вы не установите программки gccrus.exe и g++rus.exe */

setlocale ( LC_ALL , ".1251" ) ;

/* Ищем максимум из двух int */

cout << "Максимум из 1 и 2 равен "

<< maximum( 1 , 2 )

<< endl ;

/* Ищем максимум из двух double */

cout << "Максимум из 1.5 и 2.5 равен "

<< maximum( 1.5 , 2.5 )

<< endl ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

_________________

309 стр. Глава 27. Шаблоны С++

Обратите внимание на ключевое слово template, за которым следуют угловые скобки — в скобках могут содержаться заменители типов, каждому из которых предшествует слово class, или константы. В нашем случае определение функции maximum( ) использует "неизвестный тип" Т. После угловых скобок идёт обычное определение функции, которая в нашем случае возвращает большее из двух значений типа Т — типа, который будет определён позже в программе.

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

«Создание функции из шаблона называется его настройкой, или инстанцированием.»

[Советы]

Первый вызов maximum( 1 , 2 ) заставляет С++ создать версию функции, в которой Т заменяется на int. Второй вызов создаёт отдельную функцию maximum( double , double ). В результате вывод программы имеет следующий вид.

Максимум из 1 и 2 равен 2

Максимум из 1.5 и 2.5 равен 2.5

Press any key to continue...

«Будьте внимательны с терминологией. Шаблон функции не является функцией. Прототип шаблона функции — maximum< T >( Т , Т ), а функция, которую создаёт данный шаблон при замене Т на int, — maximum ( int , int ) ( это уже функция, а не шаблон ).»

[Атас!]

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

double d = maximum( 1 , 2.0 ) ;

_________________

310 стр. Часть 5. Полезные особенности

Проблема в том, что типы первого и второго аргументов различны, а при инстанцировании типы аргументов должны точно соответствовать объявлению функции. Приведённое же выражение может соответствовать шаблону maximum< T1 , Т2 > ( Т1 , Т2 ) ( тогда С++ заменит Т1 на int , а Т2 на double ), но не использовавшемуся ранее шаблону с одним аргументом типа.

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

float maximum( float , float ) ; /* Заставляет С++ */

/* инстанцировать шаблон функции */

/* maximum< T >( Т , Т ) для Т = float */

«С++ даже не пытается компилировать шаблон функции до тех пор, пока шаблон не будет преобразован в реальную функцию. Если ваш шаблон содержит ошибки, вероятно, вы не узнаете о них до тех пор, пока не инстанцируете его.»

[Атас!]

►Шаблоны классов...311

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

/* TemplateVector — реализация шаблона вектора */

#include <cstdlib>

#include <cstdio>

#include <iostream>

#include <sstream>

#include <string.h>

using namespace std ;

/* TemplateVector — простой шаблон массива */

template < class T >

class TemplateVector

{

public :

TemplateVector( int nArraySize )

{

/* Количество элементов массива */

nSize = nArraySize ;

array = new T[ nArraySize ] ;

reset( ) ;

}

int size( ) { return nWriteIndex ; }

void reset( ) { nWriteIndex = 0 ; nReadIndex = 0 ; }

void add( T object )

{

if ( nWriteIndex < nSize )

{

array[ nWriteIndex++ ] = object ;

}

}

T get( )

{

_________________

311 стр. Глава 27. Шаблоны С++

return array[ nReadIndex++ ] ;

}

protected :

int nSize ;

int nWriteIndex ;

int nReadIndex ;

T* array ;

} ;

/* Работа с двумя векторами — целых чисел и имён */

void intFn( ) ;

void nameFn( ) ;

int main( int argc , char* pArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */

intFn( ) ;

nameFn( ) ;

/* Пауза для того, чтобы посмотреть  на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

/* Работа с целыми числами */

void intFn( )

{

/* Создание вектора */

TemplateVector< int > integers( 10 ) ;

/* Добавляем значения в вектор */

cout << "Введите последовательность целых чисел\n"

"для внесения в вектор ( отрицательное\n"

"число завершает ввод последовательности )"

<< endl ;

for ( ; ; )

{

int n ;

cin >> n ;

if ( n < 0 ) { break ; }

integers.add( n ) ;

}

cout << "\nВы ввели следующие числа" << endl ;

for ( int i = 0 ; i < integers.size( ) ; i++ )

{

cout << i << ":" << integers.get( ) << endl ;

}

}

/* Работа с именами */

class Name

{

public :

Name( char* n = " " ) : name( n ) { }

_________________

312 стр. Часть 5. Полезные особенности

string display( ) { return name ; }

protected :

string name ;

} ;

void nameFn( )

{

/* Создание вектора */

TemplateVector< Name > names( 10 ) ;

/* Добавление значений в вектор */

cout << "Введите имена\n"

<< "('х' для завершения ):" << endl ;

for ( ; ; )

{

char buffer[ 80 ] ;

do

{

cin.getline( buffer , 80 ) ;

} while ( strlen( buffer ) == 0 ) ;

if ( stricmp( buffer , "x" ) == 0 )

{

break ;

}

names.add( Name( buffer ) ) ;

}

cout << "\nВы ввели имена" << endl ;

for ( int i = 0 ; i < names.size( ) ; i++ )

{

Name name = names.get( ) ;

cout << i << ":" << name.display( ) << endl ;

}

}

Шаблон класса TemplateVector< T > содержит массив объектов класса Т. Шаблон класса имеет две функции-члена: add( ) и get( ). Первая из них добавляет объект типа Т в очередное пустое место массива, а вторая — возвращает следующий элемент из массива.

Приведённая программа инстанцирует этот шаблон сначала для типа int. а затем для пользовательского класса Name.

Функция intFn( ) создаёт вектор целых чисел с 10 элементами, после чего считывает вводимые пользователем числа в вектор, а потом выводит их на экран, используя функции, предоставляемые шаблоном TemplateVector.

Вторая функция, nameFn( ), создаёт вектор объектов типа Name. Функция так же размещает пользовательский ввод в векторе, а потом выводит его элементы на экран.

Обратите внимание, как шаблон TemplateVector позволяет с одинаковой простотой работать как со встроенным типом, так и с пользовательским классом. Вот как выглядит пример работы данной программы.

Введите последовательность целых чисел

для внесения в вектор ( отрицательное число

завершает ввод последовательности )

5

10

15

-1

Вы ввели следующие числа

0:5

1:10

2:15

Введите имена

('х' для завершения ):

Igor

Ira

Anton

x

Вы ввели имена

0: Igor

1: Ira

2 : Anton

Press any key to continue...

_________________

313 стр. Глава 27. Шаблоны С++

►Зачем нужны шаблоны классов...314

"Неужели я не могу просто создать класс Array? — скажете вы. — Зачем мне возиться с шаблонами?"

Конечно, можете. Если заранее знаете, объекты какого типа будут храниться в этом массиве. Например, если вам нужен только массив целых чисел, то нет смысла ломать голову над шаблоном Vector< T > — проще создать класс IntArray и использовать его.

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

/* VoidVector — реализация вектора с использованием */

/*                 void* для хранения элементов */

#include <cstdlib>

#include <cstdio>

#include <iostream>

using namespace std ;

typedef void* VoidPtr ;

class VoidVector

{

public:

VoidVector( int nArraySize )

{

/* Количество элементов */

/* Количество элементов */

nSize = nArraySize ;

ptr = new VoidPtr[ nArraySize ] ;

reset( ) ;

}

int size( ) { return nWriteIndex ; }

void reset( ) { nWriteIndex = 0 ; nReadIndex = 0 ; }

void add( void* pValue )

{

if ( nWriteIndex < nSize )

{

ptr[ nWriteIndex++ ] = pValue ;

_________________

314 стр. Часть 5. Полезные особенности

}

}

VoidPtr get( ){ return ptr[ nReadIndex++ ] ; }

protected :

int nSize ;

int nWriteIndex ;

int nReadIndex ;

VoidPtr* ptr ;

} ;

int main( int argc , char* pArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать русских текстов */

/* Создание вектора */

VoidVector vv( 10 ) ;

/* Добавление значений к вектору */

cout << "Введите последовательность целых чисел\n"

"для внесения в вектор ( отрицательное\n"

"число завершает ввод последовательности )"

<< endl ;

for( ; ; )

{

int* p = new int ;

cin  >> *p ;

if ( *p < 0 )

{

delete p ;

break ;

}

vv.add( ( void* ) p ) ;

}

cout << "\nВы ввели следующие числа" << endl ;

for ( int i = 0 ; i < vv.size( ) ; i++ )

{

int* p = ( int* )vv.get( ) ;

cout << i << ":" << *p << endl ;

}

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

В этой программе тип VoidPtr определён как синоним void*.

«Ключевое слово typedef создаёт новое имя для существующего типа. Вы можете везде, где видите VoidPtr, в уме вставлять void*. Использование таких замен делает текст более удобочитаемым, а также упрощает синтаксис выражений. Иногда оказывается невозможным заставить работать существующий шаблон класса с указателем, и тогда использование typedef для замены составного типа наподобие указателя может решить проблему.»

[Советы]

_________________

315 стр. Глава 27. Шаблоны С++

Класс VoidVector предоставляет те же функции-члены add( ) и get( ), что и TemplateVector из предыдущей программы.

Это решение имеет ( как минимум ) три проблемы. Во-первых, оно неудобно в использовании, как видно из текста функции main( ) — вы не в состоянии сохранить значение, и должны использовать только указатели на объекты. Это означает, что вы должны выделить для значения память в куче и поместить в вектор её адрес.

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

int n ;

сin >> n ;

vv.add( ( void* ) &n ) ;

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

«На самом деле всё ещё хуже — адрес n остаётся неизменным во всех итерациях цикла for.»

[Советы]

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

double dValue = *( double* )get( ) ;

Такая программа не будет работать корректно, поскольку в dValue в результате окажется какой-то мусор. Однако компиляция этой программы пройдёт без ошибок. Приведение типа к void* сводит на нет преимущества строгой типизации С++.

►Советы по использованию шаблонов...316

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

Во-вторых, шаблон класса не потребляет память. Следовательно, наличие шаблона класса никак не скажется на программе, если этот шаблон не будет инстанцирован. С другой стороны, шаблон класса использует память при каждом инстанцировании, поэтому несмотря на то, что, например, класс Array< int > уже существует, классу Array< Student > также потребуется память.

И наконец, шаблон класса не компилируется и не проверяется на наличие ошибок до тех пор, пока не будет преобразован в реальный класс. Таким образом, программа, содержащая Аггау< Т >, может нормально компилироваться, несмотря на наличие в шаблоне очевидных синтаксических ошибок. Эти ошибки никак не проявят себя до тех пор, пока не будут созданы реальные классы наподобие Array< int > или Array< Student >.

_________________

316 стр. Часть 5. Полезные особенности

Глава 28. СТАНДАРТНАЯ БИБЛИОТЕКА ШАБЛОНОВ...317

ОГЛАВЛЕНИЕ

В этой главе...

►Контейнер string 317

►Контейнер list 320

►Итераторы 321

►Использование контейнера map 324

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

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

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

В настоящее время в состав С++ входит стандартная библиотека шаблонов ( Standard Template Library, STL ), включающая множество различных типов контейнеров, каждый из которых обладает своими достоинствами ( и, само собой, недостатками ).

«STL — весьма объёмная библиотека с массой сложно реализованных контейнеров. Весь приведённый здесь материал следует рассматривать как беглое знакомство лишь с некоторыми возможностями STL.»

[Советы]

►Контейнер string...317

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

cout << "Это обычная строка" ;

_________________

317 стр. Глава 28. Стандартная библиотека шаблонов

А вот как выглядит конкатенация двух строк:

char* concatString( char* s1 , char* s2 )

{

int length = strlen( s1 ) + strlen( s2 ) + 1 ;

char* s = new char[ length ] ;

strcpy( s , s1 ) ;

strcat( s , s2 ) ;

return s ;

}

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

string concat( string s1 , string s2 )

{

return s1 + s2 ;

}

«До сих пор в программах я старался избегать использования класса string, поскольку вы ещё с ним не знакомы. Однако большинство программистов используют этот класс гораздо чаще, чем массивы символов с завершающим нулевым элементом.»

[Помни!]

Приведённая далее программа демонстрирует несколько возможностей класса string.

/* STLString — демонстрация простейших */

/*           возможностей класса string из STL */

#include <string>

#include <cstdlib>

#include <iostream>

using namespace std ;

/* concat — конкатенация двух строк */

string concat( string s1 , string s2 )

{

return s1 + s2 ;

}

/* removeSpaces — удаление всех пробелов из строки */

string removeSpaces( string s )

{

/* Находим смещение первого пробела; продолжаем поиск до тех пор, пока не сможем найти больше ни одного пробела */

size_t offset ;

while ( ( offset = s.find( " " ) ) != -1 )

{

/* Удаляем найденный пробел */

s.erase( offset , 1 ) ;

}

return s ;

}

/* insertPhrase — вставка фразы в том месте, где находится метка <ip> */

string insertPhrase( string source )

{

_________________

318 стр. Часть 5. Полезные особенности

size_t offset = source.find( "<ip>" ) ;

if ( offset != -1 )

{

source.erase( offset , 4 ) ;

source.insert( offset , "Randall" ) ;

}

return source ;

}

int main( int argc , char* pArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

/* Создаём строку, которая представляет собой конкатенацию двух меньших строк */

cout << "string1 + string2 = "

<< concat( "string1 " , "string2" )

<< endl ;

/* Создаём тестовую строку и удаляем в ней все пробелы */

string s2( "The phrase" ) ;

cout << "<" << s2 << "> минус пробелы = <"

<< removeSpaces( s2 ) << ">" << endl ;

/* Вставляем фразу в средину существующей строки */

string s3 = "Stephen <ip> Davis" ;

cout << s3 + "  ->  " + insertPhrase( s3 ) << endl ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

Оператор operator+( ) выполняет конкатенацию строк, которая раньше осуществлялась при помощи функции concatCharacterString( ).

Функция removeSpaces( ) удаляет все найденные пробелы из строки при помощи операции string::find( ), которая возвращает смещение первого найденного пробела. После того, как положение пробела в строке определено, функция-член erase( ) удаляет его из строки. Метод find( ) возвращает смещение найденного пробела от начала строки, или -1, если он не найден.

«Тип size_t определён в заголовочных файлах STL как целое число, которое в состоянии работать с массивом максимально допустимого на вашем компьютере размера. Обычно это тип long. Использование типа size_t связано с вопросами переносимости исходного кода между различными программно-аппаратными платформами. Visual С++ .NET сгенерирует предупреждение, если вместо size_t вы используете int.»

[Советы]

Функция insertPhrase( ) использует метод find( ) для поиска точки вставки в строку, после чего метод erase( ) удаляет метку вставки <ip> из строки, а метод insert( ) вставляет новую строку в средину старой.

Вот как выглядит вывод данной программы:

string1 + string2 = string1 string2

<The phrase> минус пробелы = <Thephrase>

Stephen <ip> Davis  ->  Stephen Randall Davis

Press any key to continue...

_________________

319 стр. Глава 28. Стандартная библиотека шаблонов

►Контейнер list...320

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

Контейнер STL list хранит объекты связанными наподобие блоков детского конструктора. Объекты могут быть связаны в любом порядке, что делает данный контейнер идеальным для вставки, сортировки, объединения списков и прочих операций над объектами. Приведённая далее программа демонстрирует использование  list для сортировки набора имён .

/* STLList — использование контейнера list для */

/*            ввода и сортировки строк */

#include <list>

#include <string>

#include <cstdio>

#include <cstdlib>

#include <iostream>

/* Объявление списка строк */

using namespace std ;

list <string> names ;

int main( int argc , char* pArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

/* Ввод строк имён */

cout << "Введите имя ( или х для завершения )"

<< endl ;

while ( true )

{

string name ;

cin >> name ;

if ( ( name.compare( "x" ) == 0 ) ||

( name.compare( "X" ) == 0 ) )

{

break ;

}

names.push_back( name ) ;

}

/* Сортируем список */

names.sort( ) ;

/* Выводим отсортированный список */

/* Выводим имена, пока список не опустеет */

cout << "\nОтсортированный список:" << endl ;

while ( !names.empty( ) )

{

/* Первое имя в списке */

string name = names.front( ) ;

cout << name << endl ;

/* Удаляем это имя из списка */

names.pop_front( ) ;

}

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

_________________

320 стр. Часть 5. Полезные особенности

В этом примере определена переменная names, являющаяся списком объектов string. Программа начинает работу с чтения вводимого пользователем списка имён . Каждое введённое имя добавляется к концу списка с помощью метода push_back( ). Цикл завершается, когда пользователь вводит имя "х". Затем список имён сортируется при помощи метода sort( ) .

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

Вот пример вывода данной программы.

Введите имя ( или х для завершения )

Igor

Ira

Anton

х

Отсортированный список:

Anton

Igor

Ira

Press any key to continue...

Контейнер list предоставляет программисту массу различных возможностей, простейшие из которых — insert, swap и erase. Контейнер также позволяет программисту осуществлять итерации по списку с выполнением пользовательской функции над каждым элементом списка.

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

►Итераторы...321

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

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

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

■■■

■ Класс может вернуть итератор, который указывает на первый член коллекции.

■ Итератор можно переместить от одного элемента к следующему.

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

■■■

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

_________________

321 стр. Глава 28. Стандартная библиотека шаблонов

Приведённая далее программа использует итератор для обхода списка STL недеструктивным образом.

/* STLListUserClass — использование списка STL для */

/*                    хранения и сортировки объектов */

/*                       пользовательского класса */

#include <list>

#include <string>

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

/* Student — пример пользовательского класса */

class Student

{

public :

Student( char* pszName , int id )

{

name = new string( pszName ) ;

ssID = id ;

}

string* name ;

int ssID ;

} ;

/* Данная функция требуется для поддержки сортировки */

bool operator<( Student& s1 , Student& s2 )

{

return s1.ssID < s2.ssID ;

}

/* Определение коллекции студентов */

list <Student> students ;

int main( int argc , char* pArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

/* Добавление нескольких студентов в список */

students.push_back( *new Student( "Семенякин Сергей" , 10 ) ) ;

students.push_back( *new Student( "Редчук Александр", 5 ) ) ;

students.push_back( *new Student( "Шапран Павел" , 25 ) ) ;

students.push_back( *new Student( "Чистяков Александр" , 20 ) ) ;

students.push_back( *new Student( "Снежко Ирина" , 15 ) ) ;

/* Сортировка списка */

students.sort( ) ;

/* обход списка: */

/* 1 ) получаем итератор, который указывает на первый элемент списка */

list <Student>::iterator iter = students.begin( ) ;

/* 2 ) цикл выполняется до тех пор, пока итератор не будет указывать на конец списка */

while ( iter != students.end( ) )

{

_________________

322 стр. Часть 5. Полезные особенности

/* 3 ) Получение студента, на которого указывает итератор */

Student& s = *iter ;

cout << s.ssID << " — " << *s.name << endl ;

/* 4 ) итератор переходит к следующему элементу списка */

iter++ ;

}

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

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

«Функция sort( ) в STL требует от пользователя переопределения оператора "меньше чем". ( Это одно из тех редких мест, где требуется определение пользовательского оператора, отличного от присвоения. ) Оператор operator<( Student&, Student& ) вызывается при вычислении выражения s1 < s2, где s1 и s2 — объекты типа Student.»

[Атас!]

Программа использует итератор iter для прохода по списку. Взгляните внимательно на объявление итератора: list <Student>::iterator представляет собой итератор для контейнера list элементов типа Student. Строгая типизация ясно видна при выполнении присвоения ( шаг 3 в приведённом коде ): *iter возвращает ссылку на объект Student. Вывод данной программы выглядит следующим образом:

5 — Редчук Александр

10 — Семенякин Сергей

15 — Снежко Ирина

20 — Чистяков Александр

25 — Шапран Павел

Press any key to continue...

Как сортирует функция sort( )

Я должен разъяснить один интересный момент — откуда метод sort( ) знает, какой из двух элементов списка "больше"? Другими словами, как определяется порядок сортировки? Для ряда типов С++ определяет порядок сортировки самостоятельно. Так, например, С++ не надо пояснять, какой из двух int больше. Кроме того, STL сортирует коллекцию строк по тем же правилам, что используются в словаре.

Таким образом, программе, сортирующей имена в списке, не надо было ничего пояснять, поскольку С++ известно, как сортировать объекты типа string. Однако С++ не знает, какой из объектов student больше. Для этой цели служит глобальная функция ::operator<( Student&, Student& ). Метод sort( ) использует эту функцию для определения корректного порядка сортировки. В качестве эксперимента измените смысл оператора operator<( ) на обратный:

return s1.ssID > s2.ssID ;

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

25 — Шапран Павел

20 — Чистяков Александр

15 — Снежко Ирина

10 — Семенякин Сергей

5 — Редчук Александр

Press any key to continue...

_________________

323 стр. Глава 28. Стандартная библиотека шаблонов

►Использование контейнера map...324

Ассоциативный массив map представляет собой ещё один класс-контейнер. Имеется множество ассоциативных массивов, но все они обладают одним общим свойством — обеспечивают быстрое сохранение и выборку в соответствии с некоторым ключом или индексом. Приведённая ниже программа демонстрирует этот принцип на практике.

Например, в институте студенты могут быть зарегистрированы при помощи уникальных идентификационных номеров. Этот идентификационный номер используется во всех случаях студенческой жизни: для получения информации о студенте, при выдаче книг в библиотеке, записи в ведомость об оценках. Очень важно, чтобы любая программа могла получить информацию о студенте по его номеру быстро и эффективно.

Следующая программа демонстрирует использование ассоциативного массива студентов с идентификатором в качестве ключа.

/* STLMap — использование ассоциативного массива */

/*        для коллекции студентов, упорядоченной */

/*              по их идентификаторам */

#include <cstdio>

#include <cstdlib>

#include <iostream>

#include <sstream>

#include <string>

#include <map>

using namespace std ;

/* SC — Функция сравнения студентов, */

/*      определяющая порядок их сортировки */

struct SC

{

bool operator( )( const int id1 , const int id2 ) const

{

return id1 < id2 ;

}

} ;

/* Ассоциативный массив в действительности содержит пары, первый элемент которых является ключом, а второй — данными ( в нашем случае — классом Student ) */

class Student ;

typedef Student* SP ;

typedef pair< const int , Student* > Pair ;

typedef map< int , SP , SC > Map ;

typedef map< int , SP , SC >::iterator MapIterator ;

/* Коллекция студентов */

Map students ;

_________________

324 стр. Часть 5. Полезные особенности

/* Student — определяет важные свойства студентов, в первую очередь — ключ, используемый для выборки информации о студенте */

class Student

{

public :

Student( char* pszName , int id )

: studentIDKey( id ) , name( pszName ) { }

/* getKey — ключ, используемый в качестве индекса в ассоциативном массиве */

const int getKey( ) { return studentIDKey ; }

/* display — вывод информации на экран */

string display( )

{

ostringstream out ;

out << studentIDKey << " — " << name ;

return out.str( ) ;

}

protected :

/* Ключевое поле — идентификатор студента */

const int studentIDKey ;

/* Имя студента ( а также прочие данные ) */

string name ;

} ;

int main( int argc , char* pArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

/* Добавляем несколько студентов в коллекцию */

Student* pS ;

pS = new Student( "Алла" , 3456 ) ;

Pair* ptr = new Pair( pS -> getKey( ) , pS ) ;

students.insert( *ptr ) ;

/* Ассоциативный массив перегружает оператор индексирования для создания пары и вставки её в массив */

students[ 1234 ] = new Student( "Лариса" ,

1234 ) ;

students[ 5678 ] = new Student( "Марианна" ,

5678 ) ;

/* Проход по списку студентов. Ассоциативный массив всегда хранит элементы упорядоченными по ключу */

cout << "Отсортированный список студентов:" << endl ;

MapIterator iter = students.begin( ) ;

while ( iter != students.end( ) )

{

Pair p = *iter ;

Student* s = p.second ;

cout << s -> display( ) << endl ;

iter++ ;

}

_________________

325 стр. Глава 28. Стандартная библиотека шаблонов

/* Операторы инкремента и декремента могут использоваться для поиска предыдущего и последующего элемента */

cout << "\nИщем студента 3456" << endl ;

MapIterator p = students.find( 3456 ) ;

cout << "Найден: " << p -> second -> display( ) << endl ;

MapIterator p1 = p ;

MapIterator prior = --p1 ;

cout << "Предшественник = "

<< prior -> second -> display( ) << endl ;

MapIterator p2 = p ;

MapIterator successor = ++p2 ;

cout << "Следующий = "

<< successor -> second -> display( ) << endl ;

/* Функция find( ) возвращает итератор end( ), если искомый элемент не найден; operator[ ] возвращает NULL */

if ( students.find( 0123 ) == students.end( ) )

{

cout << "Вызов students.find( 0123 ) возвратил\n"

<< "students.end( ), т.к. студента 0123 нет"

<< endl ;

}

/* Вывод с использованием индекса */

cout << "Проверка индекса: students[ 3456 ] = "

<< students[ 3456 ] -> display( ) << endl ;

if ( students[ 0123 ] == NULL )

{

cout << "students[ 0123 ] возвращает NULL"

<< endl ;

}

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

Ключевым моментом программы являются три оператора typedef. Контейнер map содержит множество объектов Pair, каждый из которых содержит по два элемента. Первый элемент — ключ ( в нашем случае — идентификатор студента ), а второй — сам объект Student. В аргументы шаблона Map добавлен класс SC, который содержит единственный метод, сравнивающий два ключа ( это немного сложнее, чем глобальная функция, использованная в контейнере list, но эффект абсолютно тот же ).

Программа начинает работу с создания трёх объектов Pair и вносит их в список. Затем проход по контейнеру показывает, что он хранит элементы упорядоченными по ключу, так что вызов метода sort( ) нам не нужен.

Во второй части программы выполняется поиск с использованием метода find( ), а также выборка предыдущего и последующего элементов контейнера при помощи операторов инкремента и декремента.

_________________

326 стр. Часть 5. Полезные особенности

Вывод программы выглядит следующим образом:

Отсортированный список студентов:

1234 — Лариса

3456 — Алла

5678 — Марианна

Ищем студента 3456

Найден: 3456 — Алла

Предшественник = 1234 — Лариса

Следующий = 5678 — Марианна

Вызов students.find( 0123 ) возвратил

students.end( ), т.к. студента 0123 нет

Проверка индекса: students[ 3456 ] = 3456 — Алла

students[ 0123 ] возвращает NULL

Press any key to continue...

_________________

327 стр. Глава 28. Стандартная библиотека шаблонов

Часть 6.  ВЕЛИКОЛЕПНАЯ ДЕСЯТКА...329

ОГЛАВЛЕНИЕ

СОДЕРЖАНИЕ

Глава 29. ДЕСЯТЬ СПОСОБОВ ИЗБЕЖАТЬ ОШИБОК...331

Глава 30. ДЕСЯТЬ ОСНОВНЫХ ВОЗМОЖНОСТЕЙ DEV-C++...336

Глава 31. ПРОГРАММА BUDGET...343

В этой части...

Ни одна книга ...для "чайников" не может обойтись без "Великолепных десяток". В главе 29, "Десять способов избежать ошибок", описано десять способов, благодаря которым вы сможете оградить свою программу от ошибок. Многое из того, что здесь сказано, подходит и для языка С. В главе 30 , "Десять основных возможностей Dev-C++", вы познакомитесь с десятком наиболее важных опций компилятора Dev-C++.

Глава 29. ДЕСЯТЬ СПОСОБОВ ИЗБЕЖАТЬ ОШИБОК...331

ОГЛАВЛЕНИЕ

В этой главе...

►Включение всех предупреждений и сообщений об ошибках 331

►Добейтесь чистой компиляции 332

►Используйте последовательный стиль программирования 332

►Ограничивайте видимость 332

►Комментируйте свою программу 334

►Хотя бы один раз выполните программу пошагово 334

►Избегайте перегрузки операторов334

►Работа с кучей 334

►Используйте для обработки ошибок исключительные ситуации 355

►Избегайте множественного наследования 355

►Включение всех предупреждений и сообщений об ошибках...331

Синтаксис С++ позволяет проверять всё и вся. Когда компилятор встречается с конструкцией, которую он не может понять, у него не остаётся никакого выбора, кроме генерации сообщения об ошибке. И хотя компилятор честно пытается перейти к следующей строке, он даже не сочтёт нужным создать выполняемую программу.

Выключение сообщений об ошибках и предупреждений подобно отключению красных габаритных огней на вашей машине, потому что они вас раздражают. Игнорирование проблемы не заставит её исчезнуть. Если ваш компилятор имеет режим абсолютной проверки кода, включите его. И Visual С++ .NET, и Dev-C++ предоставляют режим "Включить все сообщения" ( Enable All Messages ), который должен постоянно находиться в рабочем состоянии. В конце концов эта многословность сбережёт ваше время.

Рассматривая ваш исходный код, умный компилятор С++ помимо ошибок ищет всякие подозрительные конструкции. Вот пример:

#include "student.h"

#include "class.h"

Student* addNewStudent ( Class class , char *pName ,

SSNumber ss )

{

Student pS ;

if( pName != 0 )

{

pS = new Student( pName , ss ) ;

class.addStudent( pS ) ;

}

return pS ;

}

Умный компилятор понимает, что если pName равно 0 , то переменная pS никогда не будет проинициализирована; он выводит сообщение об этом и обращает ваше внимание, что было бы неплохо взглянуть на эту проблему ещё раз.

►Добейтесь чистой компиляции...332

Не начинайте отладку кода, пока не избавитесь от всех предупреждений ( или хотя бы не поймёте их причину ). Выключение режима вывода всех сообщений об ошибках, чтобы они не надоедали, не приведёт ни к чему хорошему. Если вы не поняли сообщение сразу, посидите и разберитесь с ним. Проблемы появляются именно там, где вы чего-то не понимаете.

►Используйте последовательный стиль программирования...332

Программирование с использованием чистого последовательного стиля не только улучшает читаемость программы, но и уменьшает количество ошибок программирования. Помните: чем меньше сил будет уходить на расшифровку кода, тем больше их останется на обдумывание логики создаваемой программы. Хороший стиль программирования позволяет легко справляться с такими задачами:

■■■

■ различать имена классов, имена объектов и имена функций;

■ получать определённую информацию об объекте по его имени;

■ отличать команды препроцессора от команд С++;

■ различать блоки программы С++ по уровню отступов.

■■■

Кроме того, вы должны приучиться создавать стандартный заголовок модулей. В таких заголовках следует приводить информацию о функциях или классах, содержащихся в данном модуле, об имени автора ( имеется в виду ваше имя ), о дате создания и версии используемого компилятора, а также об истории исправлений и модификаций.

Очень важно, чтобы программисты, работающие над одним проектом, имели одинаковый стиль программирования. Попытка разобраться в программе, состоящей из фрагментов с разными стилями программирования, — довольно тоскливое занятие.

►Ограничивайте видимость...332

Ограничение видимости внутреннего содержимого класса для внешнего мира — один из краеугольных камней объектно-ориентированного программирования. Класс сам отвечает за своё содержимое, а приложение лишь использует класс для решения поставленных задач.

В частности, ограниченная видимость означает, что данные-члены не должны быть доступны извне класса, т.е. они должны быть защищёнными или закрытыми. Кроме того, должны быть защищены функции-члены, о которых внешние приложения не должны знать.

Открытые функции-члены должны как можно меньше доверять внешнему коду — любой аргумент, переданный открытой функции-члену, должен рассматриваться как потенциальный источник ошибки, пока не будет достоверно доказано обратное. Функция, приведённая ниже, представляет собой мину, которая только и ждёт своего часа.

_________________

332 стр. Часть 6. Великолепная десятка

class Array

{

public :

Array( int s )

{

size = 0 ;

pData = new int[ s ] ;

if ( pData )

{

size = s ;

}

}

~Array( )

{

delete pData ;

size = 0 ;

pData = 0 ;

}

/* Вернуть или установить данные в массиве */

int data( int index )

{

return pData[ index ] ;

}

int data ( int index , int newValue )

{

int oldValue = pData[ index ] ;

pData[ index ] = newValue ;

return oldValue ;

}

protected :

int size ;

int *pData ;

} ;

Функция data( int ) позволяет внешнему приложению читать данные из Array. Эта функция слишком доверчива: она не проверяет, находится ли переменная index в допустимом диапазоне. А что, если она выйдет за предусмотренные границы? Функция data( int , int ) с этой точки зрения ещё хуже, поскольку производит запись в неизвестное место в памяти.

В следующем примере показано, как осуществить такую проверку. Для краткости приведём только пример функции data( int ).

int data( unsigned int index )

{

if ( index >= size )

{

throw Exception( "Индекс массива вне "

"допустимого диапазона" ) ;

}

return pData[ index ] ;

}

Теперь, если переменная index будет выходить за пределы допустимого диапазона, это будет выявлено на этапе проверки ( беззнаковость переменной index избавляет от необходимости проверки на отрицательность значения индекса ).

_________________

333 стр. Глава 29. Десять способов избежать ошибок

►Комментируйте свою программу...334

Я считаю, что можно избежать излишних ошибок, если комментировать программу в процессе её написания, вместо того чтобы ждать, когда программа заработает, и только потом возвращаться и добавлять комментарии к тексту.

Понятно, что никому не хочется тратить время на написание объёмистых заголовков и пояснений к функциям, но всегда можно найти пару минут, чтобы добавить краткий комментарий к программе. Когда вы вернётесь к коду, написанному недели две назад, короткие и содержательные комментарии значительно облегчат "вживание" в программу.

Кроме того, последовательно используемые отступы и соглашения об именах сделают код гораздо доступнее для чтения и понимания. Конечно, очень приятно, когда код легко читать по завершении написания программы, но просто необходимо, чтобы код хорошо воспринимался и при написании — именно тогда, когда вам действительно нужна помощь.

►Хотя бы один раз выполните программу пошагово...334

Существенно облегчит понимание работы программы пошаговое выполнение программы с помощью хорошего отладчика ( обычно для этого вполне подходят отладчики, включенные в среды разработки компиляторов ).

Когда некоторая функция готова и может быть добавлена к программе, её следует тщательно проверить хотя бы один раз, проходя все возможные варианты ветвления потока управления. Гораздо легче отлавливать ошибки в отдельной функции, чем в комплексе с другими функциями программы.

►Избегайте перегрузки операторов...334

Настоятельно советую избегать перегрузки операторов, за исключением оператора присвоения operator = ( ), пока вы как следует не освоитесь в С++. Хотя хороший набор перегруженных операторов и может значительно повысить полезность класса и читаемость кода, перегрузка почти никогда не бывает крайне необходимой и может значительно усложнить жизнь начинающего программиста. Тот же эффект вы можете получить при помощи определения и использования соответствующих открытых функций-членов.

После того как вы интенсивно поработаете с языком С++ по меньшей мере несколько месяцев, можете начинать перегружать операторы как вам заблагорассудится.

►Работа с кучей...334

Основное правило, касающееся кучи, заключается в том, что выделение и освобождение памяти из кучи должно происходить на одном уровне. Если функция-член MyClass::create( ) выделяет блок памяти и возвращает его вызывавшему коду, то должна существовать и функция MyClass::create( ), которая освобождает блок памяти, возвращая его в кучу. MyClass::create( ) не должна требовать от вызывающей функции самостоятельного освобождения памяти. Это, конечно, не помогает избежать всех проблем ( например, вызывающая функция может просто "забыть" вызвать MyClass::create( ) ), однако всё  же снижает вероятность их возникновения.

_________________

334 стр. Часть 6. Великолепная десятка

►Используйте для обработки ошибок исключительные ситуации...335

Механизм исключений введён в С++ специально для удобства и эффективности обработки ошибок. Вы должны использовать именно его, а не возврат определённых значений в случае ошибки. Такой код легче писать, читать и поддерживать. Конечно, подходить к вопросу надо разумно — так, нет необходимости генерировать исключение в функции, которая возвращает индикатор "не работает" в половине вызовов.

►Избегайте множественного наследования...335

Множественное наследование, как и перегрузка операторов, на порядок усложняют программу, что совсем не нужно начинающему программисту. Большинство отношений реального мира могут быть выражены с помощью одиночного наследования ( некоторые утверждают, что множественное наследование вообще не нужно, но я не из таких ).

В любом случае вы можете спокойно использовать классы с множественным наследованием из коммерческих библиотек, например классы Microsoft MFC.

Лишь убедившись в том, что вы твёрдо понимаете концепции С++, можно начинать экспериментировать с множественным наследованием. Вы будете уже достаточно подготовлены к неожиданным ситуациям, которые могут возникнуть при использовании этого механизма.

_________________

335 Глава 29. Десять способов избежать ошибок

Глава 30.    ДЕСЯТЬ ОСНОВНЫХ ВОЗМОЖНОСТЕЙ DEV-C++...336

ОГЛАВЛЕНИЕ

В этой главе...

►Настройка редактора по вашему вкусу 336

►Подсветка парных скобок 337

►Включение обработки исключений 337

►Включение отладочной информации 338

►Создание файла проекта 338

►Настройка справки 338

►Переустановка точек останова после редактирования файла 339

►Избегайте некорректных имён файлов 339

►Включите заголовочные файлы в ваш проект 339

►Работа с профайлером 339

В этой главе рассматриваются некоторые установки Dev-C++, которые могут облегчить ваш программистский труд. Здесь мы также коснёмся вопроса о профайлере Dev-C++.

►Настройка редактора по вашему вкусу...336

Программирование должно доставлять удовольствие. В С++ и так достаточно сложностей, поэтому постарайтесь сделать всё  возможное, чтобы работа с редактором приносила вам как можно больше радости. Для этого в Dev-C++ воспользуйтесь командой меню Сервис => Параметры редактора ( ToolsoEditor => Options ), которая позволяет изменить настройки редактора.

Начнём с наиболее незначительных установок. Например, можно установить табуляцию, которая будет соответствовать четырём пробелам, и ширину строки, равную 60 символам ( эти параметры вы, безусловно, можете изменить на свой вкус ).

Флаг Подсветка синтаксиса ( Syntax Highlighting ) заставляет редактор выделять различным цветом слова в вашей программе — в зависимости от того, что они собой представляют: комментарий, ключевое слово, идентификатор и т.д. Многоцветность текста может сначала казаться раздражающей, но в конечном итоге она оказывается очень полезной. Вы можете поменять используемые цвета по своему усмотрению.

Опция Автоотступ ( Auto Indent ) оградит вас от вбивания необходимого количества пробелов или табуляций в начале строки для выравнивания текста и получения красивого отступа — редактор сделает это за вас, как только вы нажмёте клавишу <Enter>. К сожалению, редактор не понимает, что при закрывающей скобке, например, надо уменьшить уровень отступа. Однако к вашим услугам есть опция Backspace — обратный отступ ( Backspace Unindents ).

_________________

336 стр. Часть 6. Великолепная десятка

Обычно я отменяю опцию Использовать табуляцию ( Use Tab Character ), что заставляет редактор для позиционирования курсора использовать пробелы и только пробелы. В основном я делал это потому, что постоянно переносил текст из редактора Dev-C++ в текстовый редактор, в котором готовил данную книгу.

►Подсветка парных скобок...337

Опция Подсвечивать парные скобки ( highlight matching braces/parenthesis ) также находится в рассмотренном в предыдущем разделе окне параметров редактора. Если эта опция установлена, редактор Dev-C++ находит при вводе закрывающей скобки соответствующую открывающую. Кроме того, когда выбираете открывающую или закрывающую скобки, Dev-C++ изменяет соответствующую скобку, выделяя её шрифтом.

Эта возможность редактора оградит вас от ошибок при вводе скобок. В процессе ввода программы вы можете забыть о закрывающей скобке, так что возможность во время ввода текста убедиться, что всё  в порядке, оказывается далеко не лишней.

В Dev-C++ 4.9.8.0 имеется, однако, один серьёзный недостаток: вы не можете открыть модуль, в котором количество открывающих скобок превышает количество закрывающих. Похоже, что при открытии .срр-файла редактор сканирует его содержимое и "зависает", будучи не в состоянии найти необходимое количество закрывающих скобок.

Таким образом, если при открытии исходного файла Dev-C++ кажется зависшим, попытайтесь сделать следующее.

24. Завершите работу Dev-C++, для чего нажмите клавиши <Ctrl-Alt-Del>, выберите в появившемся окне опцию Task Manager, а в его окне — Dev-C++. После этого щёлкните на кнопке End Task.

25. Запустите Dev-C++ без открытия файла.

26. Отмените опцию Подсвечивать парные скобки ( highlight matching braces/parenthesis ).

27. Откройте ваш файл.

Последнюю версию программы вы найдёте на Web-узле www.bloodshed.net — возможно, в более новой версии эта ошибка исправлена.

►Включение обработки исключений...337

Обработка исключений рассматривалась в главе 25, "Обработка ошибок и исключения". Воспользуйтесь командой меню Сервис => Параметры компилятора ( Tools => Compiler Options ) и в открывшемся диалоговом окне выберите вкладку Настройки ( Settings ). В левой части окна выберите Генерация кода ( Code Generation ), и убедитесь, что флаг Включить обработку исключений ( Enable exception handling ) установлен равным Yes ( значение по умолчанию — No ).

При использовании обработки исключений ваша программа немного увеличится в размере и станет немного медленнее работать, это небольшая цена за возможность использовать механизм обработки исключений.

_________________

337 стр. Глава 30. Десять основных возможностей Dev-C++

►Включение отладочной информации...338

Флаг Генерировать отладочную информацию ( Generate debugging information ) можно найти на той же вкладке, выбрав в левой части окна Компоновщик ( Linker ). Этот флаг должен быть установлен равным Yes, пока идёт отладка программы ( отладчик не будет работать, если этот флаг будет иметь значение No ). Кроме того, Dev-C++ выдаст только очень небольшую информацию о происшедшем в случае аварийного завершения программы.

Когда флаг Генерировать отладочную информацию ( Generate debugging information ) равен Yes, Dev-C++ включает в выполнимый файл информацию о местоположении каждой метки и каждой строки кода ( именно поэтому отладчик знает, где следует устанавливать точки останова ). Включается даже информация о строках кода библиотечных программ.

Я скомпилировал одну из моих программ с включенным флагом генерации отладочной информации, а затем — с выключенным. В первом случае у меня получился выполнимый файл размером 1.2 Мбайта, а во втором — 440 Кбайт. Отсюда мораль: этот флаг должен быть включен во время разработки программы и выключен при сборке окончательной версии программы.

►Создание файла проекта...338

Вы можете собрать программу из единственного .срр-файла, не используя файл проекта, но построение программы из нескольких модулей требует его наличия. О том, как разбивать программу на модули и зачем это делать, а также как использовать файл проекта, вы узнали из главы 22, "Разложение классов".

►Настройка справки...338

Справка Dev-C++ по умолчанию ограничивается помощью по компилятору и не включает ни справку по С++, ни по его библиотекам. К счастью, Dev-C++ позволяет вам настроить меню Справка ( help ), добавив в него файлы в формате Microsoft Help ( .hlp ) или Compiled HTML ( .chm ). ( Примечание: вы должны сами найти соответствующие файлы, например, в Web. Ни Dev-C++, ни www.bloodshed.net не предоставляют дополнительные справочные файлы. )

В качестве примера я загрузил из Web доступный бесплатно файл справки Win32.hlp, в котором описаны вызовы интерфейса прикладного программирования ( API ) операционной системы Windows. Выберите в меню команду Справка => Настроить меню справки ( help => Customize Help Menu ) для вызова диалогового окна Редактора меню справки ( help Menu Editor ).

Щёлкните на кнопке Добавить ( Add ) в верхней части диалогового окна. Dev-C++ выведет окно открытия файла, в котором вы должны выбрать добавляемый вами файл и щёлкнуть на кнопке Open. После этого в окне редактора вы увидите добавленный вами файл справки, содержимое которого отныне будет доступно для вас через команду меню Справка ( help ).

В меню можно добавить любое количество справочных файлов.

_________________

338 стр. Часть 6. Великолепная десятка

►Переустановка точек останова после редактирования файла...339

Dev-C++ устанавливает точки останова, основываясь на номерах строк исходного текста. К сожалению, он не перемещает точки останова автоматически при вставке или удалении строк в исходном файле. Предположим, например, что я установил точку останова в 10 строке моей программы. Если после этого я добавлю комментарий между 9 и 10 строками, точка останова будет указывать на этот комментарий. Понятно, что комментарий не выполняется, так что данная точка останова теряет смысл.

Не забывайте переустанавливать точки останова после редактирования исходных файлов.

►Избегайте некорректных имён файлов...339

В плане использования имён файлов Dev-C++ не совсем корректная программа. В случае неверного имени файла компилятор генерирует сообщения об ошибках, которые способны только ввести в заблуждение. Кроме того, Dev-C++ не в состоянии работать с именами файлов и папок, которые содержат пробелы в имени.

Dev-C++ может работать с файлами в сети, в отличие от консольного окна, так что вы сможете скомпилировать программу \\Randy\MyFolder\MyProgram.срр, но не сможете отладить полученный выполнимый файл.

►Включите заголовочные файлы в ваш проект...339

С++ позволяет вам собрать инструкции, расположенные в разных файлах, в одну программу при помощи директивы препроцессора #include. Никаких ограничений на то, что может содержаться в заголовочном файле, нет. Однако следует придерживаться определённых правил и размещать в заголовочных файлах только следующее.

■■■

■ Прототипы функций.

■ Определения классов.

■ Определения шаблонов всех видов.

■ Определения всех глобальных переменных.

■■■

Вы не должны включать в заголовочные файлы выполнимые команды ( за исключением функций, определённых внутри класса ). Не забудьте также добавить включаемый файл в список файлов проекта, даже если он не содержит исходный код. Включение заголовочных файлов в проект обеспечит автоматическое перекомпилирование проекта при внесении изменений в заголовочный файл.

►Работа с профайлером...339

При написании программы вы не должны слишком сосредотачиваться на том, насколько быстро она работает ( понятно, что это не значит понапрасну тратить процессорное время на никому не нужные задачи ). Само собой, в первую очередь следует заботиться об использовании быстрых и эффективных алгоритмов, соответствующих поставленной задаче. Например, нет смысла "вылизывать" код, который сортирует большой объём данных при помощи пузырьковой сортировки, поскольку это всё  равно не сделает его быстрее, чем при использовании алгоритма быстрой сортировки.

_________________

339 стр. Глава 30. Десять основных возможностей Dev-C++

Однако если алгоритмические ресурсы исчерпаны, а программа работает слишком медленно, вам на помощь может прийти профайлер Dev-C++. Это инструмент, который выясняет, сколько времени затрачивает ваша программа на выполнение тех или иных действий, и позволяет вам сосредоточить свои усилия на наиболее "узком" месте программы.

Для включения профайлера воспользуйтесь командой меню Сервис => Параметры компилятора ( Tools => Compiler Options ), затем выберите вкладку Настройки ( Settings ), в левой части окна — Профилирование кода ( Code profiling ), а в правой установите опцию Генерировать профилирующую информацию для анализа ( Generate Profiling Info for Analysis ) равной Yes.

Давайте профилируем немного изменённую программу DeepCopy из главы 18, "Копирующий конструктор".

//

/* DeepCopy — программа для демонстрации профилирования */

//

#include <cstdio>

#include <cstdlib>

#include <iostream>

#include <strings.h>

using namespace std ;

class Person

{

public :

Person( char *pN )

{

pName = new char[ strlen( pN ) + 1 ] ;

if ( pName != 0 )

{

strcpy( pName , pN ) ;

}

}

Person( Person& p )

{

pName = new char[ strlen( p.pName ) + 1 ] ;

if ( pName != 0 )

{

strcpy( pName , p.pName ) ;

}

}

~Person( )

{

delete pName ;

pName = 0 ;

}

protected :

char *pName ;

} ;

_________________

340 стр. Часть 6. Великолепная десятка

void fn1( Person &p )

{

/* Создаём новый объект */

Person p1( p ) ;

}

void fn2( Person p )

{

/* Создаём новый объект */

Person* p1 = new Person( p ) ;

delete p1 ;

}

int main( int nNumberofArgs , char* pszArgs[ ] )

{

Person p( "Very, very long name" ) ;

for ( int i = 0 ; i < 10000000 ; i++ )

{

fn1( p ) ;

fn2( p ) ;

}

return 0 ;

}

Эта программа вызывает функции fn1( ) и fn2( ) десять миллионов раз — просто нереально получить сколь-нибудь точную картину происходящего, если программа выполняется меньше секунды.

Кроме того, я удалил из программы весь вывод на экран. Если вы вернёте его назад, то сразу убедитесь, что это очень медленный процесс, так что время, затраченное на вывод на экран, не даст возможности получить результаты по другим функциям.

После выполнения программы воспользуйтесь командой меню Выполнить => Анализ профиля ( Execute => Profile Analysis ). Появляющееся при этом окно показано на рис. 30.1.

Рис. 30.1. Анализ профиля программы

_________________

341 стр. Глава 30. Десять основных возможностей Dev-C++

Интерпретация профиля требует определённой практики. В окне показаны функции, вызываемые в процессе выполнения программы ( в программе могут быть и другие функции, которые никогда не вызываются ). В первом столбце перечислены имена функций, за которыми следуют время их работы в процентах от общего времени работы программы. В нашем случае больше всего времени занимает выполнение конструктора копирования. В столбце Self Sees указано общее время, затраченное на выполнение этой функции.

Означает ли приведённая информация, что конструктор копирования — самая медленная функция программы? Не обязательно. Просто программа вызывает эту функцию чаще других — и в функции fn1( ), и в fn2( ).

Опустившись ниже по списку, мы видим, что fn2( ) работает почти в два раза больше, чем fn1( ). В основном это связано с тем, что функция main( ) передаёт функции fn2( ) объект Person по значению, что приводит к дополнительному вызову конструктора копирования.

_________________

342 стр. Часть 6. Великолепная десятка

Глава 31. ПРОГРАММА BUDGET...343

ОГЛАВЛЕНИЕ

В этой главе...

►BUDGET1 343

►BUDGET2 348

►BUDGET3 355

►BUDGET4 366

►BUDGET5 377

В этой дополнительной главе описана серия программ для работы с воображаемым банковским счётом. Первая из них, BUDGET1, решает поставленную задачу с использованием простейших ( и подверженных ошибкам ) технологий функционального программирования. В программе BUDGET2 информация о счетах хранится в двух классах, Savings и Checking. Программа BUDGET3 выражает взаимоотношения классов Savings и Checking путём порождения их из общего класса Account. Объекты в этой программе хранятся в связанном списке. Программа BUDGET4 обобщает связанный список, превращая его в шаблон класса LinkedList< class Т >, а в программе BUDGET5 мы перестаём изобретать велосипед и используем один из контейнеров STL. Все исходные тексты программ находятся на прилагаемом компакт-диске.

►BUDGET1...343

Главы, составляющие две первые части книги, предоставляют вам достаточно информации для написания собственной нетривиальной программы. Такой программой является рассматриваемая далее программа BUDGET 1.

В этой программе используются концепции С++, рассмотренные в первых двух частях книги. Программа BUDGET моделирует простейший банковский счёт ( в очень упрощённом виде ). Эта программа будет выполнять такие действия:

■■■

■ создавать один или несколько банковских счетов;

■ присваивать уникальный номер каждому счёту;

■ работать со счётом — создание депозита и снятие денег;

■ выводить окончательный баланс всех счетов, после того как пользователь решит выйти из программы.

■■■

Программа имитирует банковские транзакции, подчиняющиеся следующим правилам ( в процессе развития программы к этим правилам будут добавлены и другие ).

■■■

■ Баланс не может быть отрицательным ( каким бы дружественным не был к вам ваш банк, настолько дружественным он быть просто не в состоянии ).

■ Создание депозита не влечёт за собой никаких расходов.

■■■

_________________

343 стр. Глава 31. Программа BUDGET

Вот как выглядит исходный текст данной программы.

/* BUDGET1.CPP — "Функциональная" программа бюджета */

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

/* Максимальное количество счетов */

const int maxAccounts = 10 ;

/* Данные, описывающие счёт */

unsigned accountNumber[ maxAccounts ] ;

double balance[ maxAccounts ] ;

/* Прототипы функций */

void process(  unsigned& accountNumber ,

double& balance ) ;

void init( unsigned* pAccountNumber ,

double* pBalance ) ;

/* main — собирает начальные входные данные и выводит конечные суммы */

int main( int nNumberofArgs , char* pszArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

cout << "Нажмите С для продолжения, X для выхода:\n" ;

// Цикл ввода

int noAccounts = 0 ; /* Количество счетов */

/* Нельзя создать счетов больше, чем выделено места */

cout << "Программа создаёт банковские счета\n" << endl ;

while ( noAccounts < maxAccounts )

{

char transactionType ;

cout << "С для создания счёта, X для выхода: " ;

cin >> transactionType ;

/* Выход, если пользователь ввёл X */

if ( transactionType == 'x' ||

transactionType == 'X' )

{

break ;

}

/* Если введено С */

if ( transactionType == 'c' ||

transactionType == 'C' )

{

/* Инициализация нового счёта */

init( &accountNumber[ noAccounts ] ,

&balance[ noAccounts ] ) ;

/* Ввод информации о транзакции */

process(  accountNumber[ noAccounts ] ,

balance[ noAccounts ] ) ;

/* Увеличение индекса */

_________________

344 стр. Часть 6. Великолепная десятка

noAccounts++ ;

}

}

double total = 0 ;

cout << "Информация о счёте\n" ;

for ( int i = 0 ; i < noAccounts ; i++ )

{

cout << "Баланс счёта "

<< accountNumber[ i ]

<< " = "

<< balance[ i ]

<< "\n" ;

/* Накопление общей информации */

total += balance[ i ] ;

}

// Вывод

cout << "Баланс по всем счетам = "

<< total

<< "\n" ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ;

return 0 ;

}

/* init — Инициализация счёта путём чтения номера и обнуления баланса */

void init( unsigned* pAccountNumber ,

double* pBalance )

{

cout << "Введите номер счёта:" ;

cin >> *pAccountNumber ;

*pBalance = 0.0 ;

}

/* process — Обновление баланса */

void process(  unsigned& accountNumber ,

double& balance )

{

cout << "Введите положительную сумму вклада,\n"

<< "отрицательную сумму для снятия со счёта\n"

<< "или нуль для завершения работы\n"

<< endl ;

double transaction ;

do

{

cout << ":" ;

cin >> transaction ;

// Вклад

if ( transaction > 0 )

_________________

345 стр. Глава 31. Программа BUDGET

{

/* Добавление на счёт */

balance += transaction ;

}

/* Снятие со счёта */

if ( transaction < 0 )

{

transaction = -transaction ;

if ( balance < transaction )

{

cout << "Недостаточно денег: всего"

<< balance

<< ", снимаем "

<< transaction

<<" \n" ;

}

else

{

balance -= transaction ;

}

}

} while ( transaction != 0 ) ;

}

Демонстрация работы данной программы:

Нажмите С для продолжения, X для выхода:

Программа создаёт банковские счета

С для создания счёта, X для выхода: С

Введите номер счёта: 1234

Введите положительную сумму вклада,

отрицательную сумму для снятия со счёта

или нуль для завершения работы

: 200

: -100

: -200

Недостаточно денег: всего 100 , снимаем 200

: 0

С для создания счёта, X для выхода: с

Введите номер счёта: 2345

Введите положительную сумму вклада,

отрицательную сумму для снятия со счёта

или нуль для завершения работы

: 200

: -50

: -50

: 0

С для создания счёта, X для выхода: х

Информация о счёте

Баланс счёта 1234 = 100

Баланс счёта 2345 = 100

Баланс по всем счетам = 200

Press any key to continue...

_________________

346 стр. Часть 6. Великолепная десятка

Разберёмся в том, как работает BUDGET. В этой программе было создано два массива, один из которых содержит номера счетов, а второй — балансы. Эти массивы синхронизированы таким образом, что элемент balance[ n ] содержит баланс счёта с номером из accountNumber[ n ], независимо от значения n. В связи с ограничением длины массива количество счетов, содержащихся в программе, не может превышать MAXACCOUNTS.

Главная программа разделена на две части: первая отвечает за сбор информации ( в ней происходит считывание размеров вкладов, снятие денег и запись результата ), а вторая — за вывод информации. Фрагмент, отвечающий за сбор информации, организован в виде цикла, в котором счета обрабатываются каждый в отдельности. В начале цикла пользователю предлагается ввести С для продолжения работы и X — для завершения. Если был введён символ X, происходит выход из цикла и переход во вторую часть main( ).

Программа выходит из цикла, если количество созданных счетов достигло MAXACCOUNTS, независимо от того, был ли введён X.

Обратите внимание, что происходит проверка введённого символа на равенство как 'X', так и 'х' — ведь в отличие от компьютера человек может не обратить внимания на регистр вводимых символов.

Если пользователь ввёл 'С', то управление передаётся функции init( ), которая создаёт счёт и заполняет его необходимой информацией. После этого функция process(  ) добавляет в счёт информацию о транзакции.

Аргументами функций init( ) и process(  ) являются указатели, так что эти функции могут изменять значения своих аргументов. В противном случае обновленная информация о счёте была бы утрачена по окончании работы функций.

После того как создание счетов завершено, управление переходит блоку, отвечающему за вывод итоговых результатов. В этом блоке происходит считывание каждого счёта и вывод баланса каждого из них. В конце выводится общая сумма.

Функция init( ) создаёт новый счёт после приглашения ввести его номер и обнуляет создаваемый счёт.

Очень важно не забыть проинициализировать новый элемент. Нулевой баланс счёта лучше непредсказуемого значения ( например, отрицательного ).

Функция process(  ) использует цикл для ввода каждой новой транзакции. Положительные значения считаются вкладом, а отрицательные — снятием со счёта. Для завершения работы со счётом достаточно ввести нуль.

Значение "0" используется программой в качестве флага. Это хотя и довольно распространённый, но не самый хороший метод. Я использовал его в этой программе только потому, что он помогает сэкономить довольно много места.

Существует множество способов улучшить программу BUDGET. Их поиск станет основой для последующего изучения языка С++. В конце книги вы увидите, как эта программа превратится в полнофункциональную объектно-ориентированную программу на С++.

Стиль программирования...347

Вы, наверное, заметили, что я пытался быть последовательным в отступах и в именовании переменных. Наша голова имеет ограниченную "производительность". И эту производительность следует направлять на создание работающих программ, а не на расшифровку уже написанных, которые невозможно прочитать из-за плохого оформления.

Важно, чтобы вы научились правильно именовать свои переменные, корректно располагать скобки и выполнять многое другое, что составляет стиль программирования. Разработав стиль программирования, придерживайтесь его, и он войдёт в привычку. Однажды вы заметите, что пишете программы быстрее, а их чтение не вызывает затруднений.

_________________

347 стр. Глава 31. Программа BUDGET

Это особенно важно, когда над проектом работают несколько программистов, поскольку правильный стиль помогает избежать проблем, которые обрушились на строителей Вавилонской башни. Кроме того, я бы настоятельно советовал тщательно разбираться в каждом сообщении об ошибке или предупреждении компилятора. Даже если считать, что предупреждение — это ещё не ошибка, то зачем дожидаться, пока оно превратится в ошибку? Тем более, что, если предупреждение такое простое, каким кажется, разобраться в нём и устранить его не составит труда. В большинстве случаев предупреждения вызываются ошибочным стилем программирования, который лучше исправить. Одни говорят, что недосмотры — это их личное дело, другие же считают, что это лишняя трата времени. Однако в любом случае обидно будет обнаружить ошибку, о которой компилятор предупреждал вас давным-давно.

►BUDGET2...348

Программа BUDGET2 представляет собой результат преобразования программы BUDGET1, основанной на использовании функций, в объектно-ориентированную программу с применением классов.

«Для того чтобы разобраться в программе BUDGET2, вы должны быть хорошо знакомы с концепциями, представленными в третьей части книги.»

[Атас!]

В программе будет решаться задача по представлению счетов в том виде, какой они имеют в банке. Эта простая программа будет поддерживать возможности вклада ( это хорошо ) и снятия денег со счёта ( это ещё лучше ). ( Первоначальная версия работала только с одним типом банковского счёта. ) Эта версия поддерживает два типа счетов, каждый из которых будет иметь собственные, несколько отличные от другого счёта правила.

Чековый счёт:

■■■

■ удерживать 20 центов за каждый обработанный чек, если баланс падает ниже 500 долларов;

■ не удерживать 20 центов, если баланс больше 500 долларов.

■■■

Сберегательный счёт:

■■■

■ не удерживать денег при первом снятии со счёта за месяц;

■ удерживать 5 долларов за каждое последующее снятие.

■■■

Рассматривая эту задачу, можно сразу отметить, что главными кандидатами на роль классов являются Checking и Savings. Поскольку данные-члены лучше сделать защищёнными, нам понадобится несколько функций, обеспечивающих доступ к номеру и балансу счёта.

Как и любой класс, Checking и Savings нуждаются в конструкторе, чтобы проинициализировать объекты правильными значениями ( как минимум, обнулить баланс ). Кроме того, понадобятся ещё две функции — deposit( ) ( вклад ) и withdrawal( ) ( снятие ).

И наконец, в этой программе я добавил ещё одну функцию-член, которая называется display( ) ; она отображает текущий объект. Это необязательное требование, однако обычно так и поступают, позволяя объекту самому заниматься своим отображением, не полагаясь на внешнюю функцию ( которой для правильного отображения могут понадобиться сведения о внутреннем устройстве класса или другая информация, которую вы, возможно, не захотите открывать ).

Вот текст этой программы:

/* BUDGET2.CPP — программа бюджета, основанная на классах */

#include <cstdio>

#include <cstdlib>

#include <iostream>

_________________

348 стр. Часть 6. Великолепная десятка

using namespace std ;

/* Максимальное количество счетов */

const int maxAccounts = 10 ;

/* Checking — здесь описан чековый счёт */

class Checking

{

public :

Checking( int initializeAN = 0 )

:accountNumber( initializeAN ) , balance( 0.0 ){ }

/* Функции обращения */

int accountNo( )

{

return accountNumber ;

}

double acntBalance( )

{

return balance ;

}

/* Функции транзакций */

void deposit( double amount )

{

balance += amount ;

}

void withdrawal( double amount ) ;

/* Функция вывода объекта в cout */

void display( )

{

cout << "Счёт " << accountNumber

<< " = " << balance

<< "\n" ;

}

protected :

unsigned accountNumber ;

double balance ;

} ;

/* withdrawal — эта функция-член слишком */

/*             велика для inline-функции */

void Checking::withdrawal( double amount )

{

if ( balance < amount )

{

cout << "Недостаточно денег: баланс равен "

<< balance

<< ", сумма чека равна " << amount

<< "\n" ;

}

else

{

balance -= amount ;

/* Если баланс падает слишком низко... */

if ( balance < 500.00 )

{

_________________

349 стр. Глава 31. Программа BUDGET

/* ...удержать деньги за обслуживание */

balance -= 0.20 ;

}

}

}

/* Savings — вы и сами можете написать этот класс */

class Savings

{

public :

Savings( int initialAN = 0 )

: accountNumber( initialAN ) ,

balance( 0.0 ) , noWithdrawals( 0 ) { }

/* функции обращения */

int accountNo( )

{

return accountNumber ;

}

double acntBalance( )

{

return balance ;

}

/* функции транзакций */

void deposit( double amount )

{

balance += amount ;

}

void withdrawal( double amount ) ;

/* Функция display — отображает объект */

void display( )

{

cout << "Счёт " << accountNumber

<< " = " << balance

<< " ( номер снятия = "

<< noWithdrawals

<< " )\n" ;

}

protected :

unsigned accountNumber ;

double balance ;

int noWithdrawals ;

} ;

void Savings::withdrawal( double amount )

{

if ( balance < amount )

{

cout << "Недостаточно денег на счёте: "

<< "баланс равен " << balance

<< ", снимается " << amount

<< "\n" ;

}

else

{

/* После первого в месяце снятия денег... */

_________________

350 стр. Часть 6. Великолепная десятка

if ( ++noWithdrawals > 1 )

{

/* ...удерживать $5 */

balance -= 5.00 ;

}

/* Снять деньги */

balance -= amount ;

}

}

/* Объявление прототипов */

void process( Checking* pChecking ) ;

void process( Savings* pSavings ) ;

/* Объекты чековых и сберегательных счетов */

Checking* chkAcnts[ maxAccounts ] ;

Savings* svgAcnts[ maxAccounts ] ;

/* main — собирает и выводит данные */

int main( int argcs , char* pArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

/* Повторять цикл до ввода 'X' или 'х' */

int noChkAccounts = 0 ; /* Содержит количество счетов */

int noSvgAccounts = 0 ;

char accountType ; /* Тип счёта — 'S' или 'С' */

while ( 1 )

{

cout << "Введите S для сберегательных счетов, " << "\n"

<< "С для чековых, "

<< "X для выхода:" ;

cin >> accountType ;

/* Выйти из цикла, если пользователь введёт X */

if ( accountType == 'x' || accountType == 'X' )

{

break ;

}

/* В противном случае обрабатывать соответствующий счёт */

switch ( accountType )

{

/* чековые счета */

case 'c' :

case 'C' :

if ( noChkAccounts < maxAccounts )

{

int acnt ;

cout << "Введите номер счёта:" ;

cin >> acnt ;

chkAcnts[ noChkAccounts ] = new Checking( acnt ) ;

process( chkAcnts[ noChkAccounts ] ) ;

noChkAccounts++ ;

}

else

{

cout << "Для чековых счетов больше нет места\n" ;

_________________

351 стр. Глава 31. Программа BUDGET

}

break ;

/* сберегательные счета */

case 's' :

case 'S' :

if ( noSvgAccounts < maxAccounts )

{

int acnt ;

cout << "Введите номер счёта:" ;

cin >> acnt ;

svgAcnts[ noSvgAccounts ] = new Savings( acnt ) ;

process( svgAcnts[ noSvgAccounts ] ) ;

noSvgAccounts++ ;

}

else

{

cout << "Для сберегательных счетов "

<< "больше нет места\n" ;

}

break ;

default :

cout << "Непонятный символ...\n" ;

}

}

/* А теперь показать общую сумму */

double chkTotal = 0 ;

cout << "Чековые счета:\n" ;

for ( int i = 0 ; i < noChkAccounts ; i++ )

{

chkAcnts[ i ]  ->  display( ) ;

chkTotal += chkAcnts[ i ]  ->  acntBalance( ) ;

}

double svgTotal = 0 ;

cout << "Сберегательные счета:\n" ;

for ( int j = 0 ; j < noSvgAccounts ; j++ )

{

svgAcnts[ j ]  ->  display( ) ;

svgTotal += svgAcnts[ j ]  ->  acntBalance( ) ;

}

double total = chkTotal + svgTotal ;

cout << "Сумма по чековым счетам = "

<< chkTotal

<< "\n" ;

cout << "Сумма по сберегательным счетам = "

<< svgTotal

<< "\n" ;

cout << "Общая сумма    = "

<< total

<< "\n" ;

_________________

352 стр. Часть 6. Великолепная десятка

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

/* обработка( Checking ) — ввод данных по чековым счетам */

void process( Checking* pChecking )

{

cout << "Введите положительное число для вклада,\n"

<< "отрицательное для снятия, 0 для завершения\n" ;

double transaction ;

do

{

cout << ":" ;

cin >> transaction ;

// Вклад

if ( transaction > 0 )

{

pChecking -> deposit( transaction ) ;

}

// Снятие

if ( transaction < 0 )

{

pChecking -> withdrawal( -transaction ) ;

}

} while ( transaction != 0 ) ;

}

/* обработка( Savings ) — ввод данных для сберегательных счетов */

void process( Savings* pSavings )

{

cout << "Введите положительное число для вклада,\n"

<< "отрицательное для снятия, 0 для завершения\n" ;

double transaction ;

do

{

cout << ":" ;

cin >> transaction ;

// Вклад

if ( transaction > 0 )

{

pSavings -> deposit( transaction ) ;

}

// Снятие

if ( transaction < 0 )

{

pSavings -> withdrawal( -transaction ) ;

}

} while ( transaction != 0 ) ;

}

_________________

353 стр. Глава 31. Программа BUDGET

Я запустил эту программу с приведёнными ниже данными для того, чтобы продемонстрировать, как она работает. Жирным шрифтом выделен пользовательский ввод, а обычным представлены сообщения программы.

Введите S для сберегательных счетов,

С для чековых, X для выхода:S

Введите номер счёта:123

Введите положительное число для вклада,

отрицательное для снятия, 0 для завершения

: 200

: -20

: 0

Введите S для сберегательных счетов,

С для чековых, X для выхода:S

Введите номер счёта:234

Введите положительное число для вклада,

отрицательное для снятия, 0 для завершения

: 200

: -10

: -10

: 0

Введите S для сберегательных счетов,

С для чековых, X для выхода:С

Введите номер счёта:345

Введите положительное число для вклада,

отрицательное для снятия, 0 для завершения

: 200

: -20

: 0

Введите S для сберегательных счетов,

С для чековых, X для выхода:С

Введите номер счёта:456

Введите положительное число для вклада,

отрицательное для снятия, 0 для завершения

: 600

: -20

: 0

Введите S для сберегательных счетов,

С для чековых, X для выхода:Х

Чековые счета:

Счёт 345 = 179.8

Счёт 456 = 580

Сберегательные счета:

Счёт 123 = 180 ( номер снятия = 1 )

Счёт 234 = 175 ( номер снятия = 2 )

Сумма по чековым счетам = 759.8

Сумма по сберегательным счетам = 355

Общая сумма      = 1114.8

Press any key to continue...

Рассмотрим каждую из функций-членов, начиная с класса Checking. Конструктор присваивает счёту его номер. Значение по умолчанию "= 0" позволяет программе создавать объект с номером счёта по умолчанию, равным нулю.

Checking c1 = new Checking( 124 ) ;

Checking с2 = new Checking( ) ;

_________________

354 стр. Часть 6. Великолепная десятка

В данном случае объект c1 класса Checking создаётся с номером счёта, равным 123, тогда как объект с2 создаётся с номером счёта по умолчанию, который равен нулю.

Функции accountNo( ) и acntBalance( ) предоставляют внешнему миру доступ к защищённым членам accountNumber и balance. Задачей этих функций является предоставление внешним функциям — не членам значений, изменить которые невозможно. Кроме того, эти функции, обеспечивающие доступ к членам, предохраняют внешние функции от необходимости внесения изменений при переменах в методе хранения номера счёта или баланса.

Функции deposit( ) и withdrawal( ) отвечают за вложение и снятие денег со счёта. Поскольку функция deposit( ) довольно проста, она была определена как inline-функция. Функция withdrawal( ), будучи несколько сложнее, объявлена в классе, но определяется позже.

Функция display( ) выводит важные данные на устройство стандартного вывода.

Класс Savings, в сущности, идентичен классу Checking, за исключением дополнительного члена noWithdrawals, который отслеживает количество проведённых снятий.

Место под объекты сберегательного и чекового счёта выделяется в массивах svgAcnts и chkAcnts соответственно. Максимальное количество счетов определено величиной maxAccounts.

Функция main( ) несколько сложнее своей сестры из программы BUDGET1, поскольку она имеет дело с двумя разными типами счетов. После проверки ввода на равенство "X" функция main( ) использует конструкцию switch, чтобы выбрать тип счёта: С для чекового и S для сберегательного. Конструкция switch использована в этой программе по двум причинам: во-первых, её проще расширить, добавляя к ней дополнительные варианты; во-вторых, она предоставляет вариант default ( вариант по умолчанию ) для обработки неверного ввода.

Как и ранее, вторая часть функции main( ) обеспечивает отображение информации о счёте, собранной в первой части этой функции.

Обратите внимание на то, как содержимое классов Checking и Savings скрыто от main( ). Так, например, main( ) просит объект показать своё содержимое, однако при этом не имеет никакого представления о том, как класс выбирает, что именно и как это показать.

Функция process( ), которая обрабатывает текущие вложения и снятия, полагается на функции-члены deposit( ) и withdrawal( ), которые выполняют за неё всю чёрную работу. Хотя вы и знаете, как именно выполняются эти действия, помните, что process(  ) об этом не имеет никакого понятия. Работа счёта касается только самого класса счёта.

Советую пройти эту программу в пошаговом режиме. Ничто иное не даст более полного представления о программе, чем её рассмотрение в действии.

Хотите — верьте, хотите — нет, но с позиции программирования BUDGET2 разрабатывается легче, чем BUDGET1. Когда я писал класс Savings, я не должен был волноваться о том, как он будет использоваться главной программой ( то же относится и к классу Checking ). Когда же я работал над функцией main( ), то не думал о содержимом класса.

Однако в этой программе есть один небольшой недостаток: классы Savings и Checking имеют очень много общего, и хотелось бы найти возможность уменьшить количество повторений кода. Эта возможность реализована в очередной версии нашей программы — BUDGET3.

►BUDGET3...355

Здесь продолжается преобразование исключительно функциональной версии программы BUDGET1, которая затем прошла через объектно-основанный этап своей эволюции — BUDGET2, в объектно-ориентированную программу BUDGET3.

_________________

355 стр. Глава 31. Программа BUDGET

«В этой программе использованы концепции, представленные в четвёртой части книги.»

[Помни!]

Программа осуществляет вложение денег на счёт и снятие со счёта в воображаемом банке. Пользователь поочередно вводит номера банковских счетов и суммы вкладов на этот счёт и снятий с него. После того как пользователь выполнил все транзакции, программа показывает баланс каждого счёта и общий баланс. Обе программы — BUDGET2 и BUDGET3 — эмулируют Checking ( чековый ) и Savings ( сберегательный ) счета. Чековые счета взимают небольшой гонорар за обслуживание при каждом снятии, если баланс упал ниже 500 долларов, тогда как сберегательный счёт взимает большой гонорар за обслуживание при первом снятии, независимо от баланса.

Программа BUDGET2 превосходит BUDGET1 только в одном: она изолирует особенности классов, описывающих счёт, от внешних функций, которые манипулировали счетами. К сожалению, BUDGET2 содержала большое количество дублированного кода в классах Savings и Checking, и именно от него мы и хотим избавиться, используя принципы наследования.

Программа BUDGET3 вносит новые улучшения. С помощью такого "супергероя" объектно-ориентированного программирования, как наследование, и его верного бокового удара — полиморфизма мы смогли оптимизировать два класса счетов, объединив в один класс Account всё  то общее, что присуще этим двум классам, и получив в результате меньшее и более компактное множество классов.

Кроме того, вместо массивов в программе используется связанный список для хранения объектов Account, что снимает ограничение на количество счетов, присущее более ранним версиям программы. Кроме того, код программы разделён на модули с различными пространствами имён .

Реализация модуля со связанным списком...356

Основное ограничение, присущее первым двум версиям программы, заключается в том, что они обе используют для хранения счетов массивы фиксированного размера, так что количество счетов, которые эти программы могут обработать, изначально ограничено. Более разумное решение состоит в использовании связанных списков объектов. Проблема лишь в том, что связанные списки ничего не знают о банковских счетах.

Очевидно, что можно разделить концепцию связанных списков на два класса. Этот тип разделения хотя и отличен от рассматриваемого в книге, но не менее важен. Он разносит классы с отношением СОДЕРЖИТ в различные файлы.

«Контейнер ( такой как массив или связанный список ) СОДЕРЖИТ счета.»

[Помни!]

Интерфейс соответствующего класса определён в заголовочном файле AccountLinkedList.h

/* AccountLinkedList — поддерживает связанный */

/*                     список объектов Account */

#ifndef _ACCOUNTLINKEDLIST_

#define _ACCOUNTLINKEDLIST_

/* Данное предварительное объявление — неприятное следствие того, что Account не является частью пространства имён */

_________________

356 стр. Часть 6. Великолепная десятка

/* Lists. Этой неприятности мы сумеем избежать в следующей версии программы */

class Account ;

namespace Lists

{

/* Предварительное объявление классов */

class AccountLinkedList ;

class Node ;

/* LinkedList — связанный  список объектов Node */

class AccountLinkedList

{

public :

AccountLinkedList( ) { pHead = 0 ; }

void addNode( Node* pNode ) ;

Node* firstNode( ) { return pHead ; }

protected :

Node* pHead ;

} ;

/* Node — узел в связанном списке, указывающий на объект Account */

class Node

{

friend class AccountLinkedList ;

public :

Node( AccountLinkedList* pL , Account* pAcc )

{

pList = pL ;

pNext = 0 ;

pAccount = pAcc ;

pL -> addNode( this ) ;

}

static Node* firstNode( AccountLinkedList* pList )

{

return pList -> firstNode( ) ;

}

Node* nextNode( ) { return pNext ; }

Account* currentAccount( ) { return pAccount ; }

protected :

AccountLinkedList* pList ;

Node* pNext ;

Account* pAccount ;

} ;

}

#endif

Файл AccountLinkedList.cpp реализует простой связанный список банковских счетов.

_________________

357 стр. Глава 31. Программа BUDGET

/* AccountLinkedList — поддерживает связанный список объектов Account */

#include "AccountLinkedList.h"

namespace Lists

{

/* addNode — добавляет узел в начало текущего связанного списка */

void AccountLinkedList::addNode( Node* pNode )

{

pNode -> pNext = pHead ;

pHead = pNode ;

}

}

Каждый объект Node связан со своим объектом Account. Указатель Node::pNext указывает на следующий счёт в списке. Объект AccountLinkedList представляет весь связанный список целиком; указатель AccountLinkedList::pHead указывает на первый объект Node в списке. Для простоты функция addNode( ) добавляет объекты Node в начало списка.

Работа со счетами...358

Данная версия программы BUDGET использует связанный список, реализованный в файле AccountLinkedList.срр. Этот класс позволяет программе хранить и работать с количеством счетов, ограниченным только объёмом свободной памяти.

Файл BUDGET3.срр представляет собой главный модуль программы, в котором содержится код приложения.

//

/* BUDGET3.СРР — Программа банковского бюджета с наследованием и полиморфизмом. Теперь одна функция может обрабатывать и чековые, и сберегательные счета ( а также любые другие, которые вы можете придумать в будущем ). */

//

/* Кроме того, вместо массива, который может иметь только определённую длину, эта версия хранит счета в связанном списке. */

#include <cstdio>

#include <cstdlib>

#include <iostream>

#include "AccountLinkedList.h"

using namespace std ;

using namespace Lists ;

/* Account — абстрактный класс, включающий общие свойства различных счетов */

class Account

{

public :

Account::Account( AccountLinkedList* pList , int accNo )

: node( pList , this )

{

/* Инициализация данных-членов */

accountNumber = accNo ;

balance = 0 ;

count++ ;

}

_________________

358 стр. Часть 6. Великолепная десятка

/* Функции доступа */

int accountNo( ) { return accountNumber ; }

double acntBalance( ) { return balance ; }

static int noAccounts( ) { return count ; }

/* Функции транзакций */

void deposit( double amount ) { balance += amount ; }

virtual bool withdrawal( double amount )

{

if ( balance < amount )

{

cout << "Недостаточно денег: на счету " << balance

<<", снимаем "            << amount

<< endl ;

return false ;

}

balance -= amount ;

return true ;

}

/* Функция вывода на экран */

void display( )

{

cout << type( )

<< " счёт " << accountNumber

<< " = "    << balance

<< endl ;

}

virtual char* type( ) = 0 ;

protected :

/* Информация о связанном списке */

Node node ;

static int count ; /* Количество счетов */

unsigned accountNumber ;

double balance ;

} ;

/* Переменная для сбора статистики */

int Account::count = 0 ;

/* Checking — свойства, уникальные для чекового счёта */

class Checking : public Account

{

public :

Checking::Checking( AccountLinkedList* pLL ,

unsigned accNo ) :

Account( pLL , accNo )

{ }

/* Перегрузка чисто виртуальных функций */

virtual bool withdrawal( double amount ) ;

virtual char* type( ) { return "Чековый" ; }

} ;

/* withdrawal — перегрузка Account::withdrawal( ) */

_________________

359 стр. Глава 31. Программа BUDGET

bool Checking::withdrawal( double amount )

{

bool success = Account::withdrawal( amount ) ;

if ( success && balance < 500.00 )

{

balance -= 0.20 ;

}

return success ;

}

/* Savings — свойства, уникальные для сберегательного счёта */

class Savings : public Account

{

public :

Savings::Savings( AccountLinkedList* pLL ,

unsigned accNo ) :

Account( pLL , accNo )

{ noWithdrawals = 0 ; }

/* Функции транзакций */

virtual bool withdrawal( double amount ) ;

virtual char* type( ) { return "Сберегательный" ; }

protected :

int noWithdrawals ;

} ;

/* withdrawal — перегрузка Account::withdrawal( ) */

bool Savings::withdrawal( double amount )

{

if ( ++noWithdrawals > 1 )

{

balance -= 5.00 ;

}

return Account::withdrawal( amount ) ;

}

/* Прототипы функций */

unsigned getAccntNo( ) ;

void   process( Account* pAccount ) ;

void   getAccounts( AccountLinkedList* pLinkedList ) ;

void   displayResults( AccountLinkedList* pLinkedList ) ;

/* main — собирает и выводит данные */

int main( int argcs , char* pArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

/* Создание связанного списка */

AccountLinkedList* pLinkedList = new AccountLinkedList( ) ;

/* Чтение пользовательского ввода */

getAccounts( pLinkedList ) ;

/* Вывод связанного списка */

displayResults( pLinkedList ) ;

_________________

360 стр. Часть 6. Великолепная десятка

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

/* getAccounts — загрузка массива счетов */

void getAccounts( AccountLinkedList* pLinkedList )

{

Account* pA ;

/* Цикл, пока не введено 'X' или 'х' */

char accountType ;       /* S или С */

while ( true )

{

cout << "Введите S для сберегательного счёта,\n"

<< "С для чекового, X для выхода: " ;

cin >> accountType ;

switch ( accountType )

{

case 'c' :

case 'C' :

pA = new Checking( pLinkedList , getAccntNo( ) ) ;

break ;

case 's' :

case 'S' :

pA = new Savings( pLinkedList , getAccntNo( ) ) ;

break ;

case 'x' :

case 'X' :

return ;

default :

cout << "Неверный ввод.\n" ;

}

/* Обработка вновь созданного объекта */

process( pA ) ;

}

}

/* displayResults — вывод информации о счетах в связанном списке */

void displayResults( AccountLinkedList* pLinkedList )

{

double total = 0.0 ;

cout << "\nИтоговая информация:\n" ;

for ( Node* pN = Node::firstNode( pLinkedList ) ;

pN != 0 ;

pN = pN -> nextNode( ) )

{

Account* pA = pN -> currentAccount( ) ;

pA -> display( ) ;

total += pA -> acntBalance( ) ;

}

_________________

361 стр. Глава 31. Программа BUDGET

cout << "Итого = " << total << "\n" ;

}

/* getAccntNo — номер счёта для его создания */

unsigned getAccntNo( )

{

unsigned accntNo ;

cout << "Введите номер счёта: " ;

cin >> accntNo ;

return accntNo ;

}

/* process( Account ) — обработка счёта */

void process( Account* pAccount )

{

cout << "Введите положительное число для вклада,\n"

<< "отрицательное для снятия,"

<< " 0 для завершения работы\n" ;

double transaction ;

do

{

cout << ":" ;

cin >> transaction ;

// Вклад

if ( transaction > 0 )

{

pAccount -> deposit( transaction ) ;

}

// Снятие

if ( transaction < 0 )

{

pAccount -> withdrawal( -transaction ) ;

}

} while ( transaction != 0 ) ;

}

Первый класс, содержащийся в BUDGET3, — Account. Этот класс инкапсулирует всё, что мы знаем об обобщённых счетах, а именно:

■■■

■ они распознаются по номерам;

■ каждый счёт имеет баланс;

■ пользователь может вкладывать или снимать деньги со счёта.

■■■

В этом классе сведён воедино код, общий для классов Savings и Checking из программы BUDGET2. Теперь Savings и Checking представляют собой подклассы Account. Я сделал функцию Account::type( ) чисто виртуальной, так что она обязана быть переопределена в каждом из подклассов.

Конструктор Account создаёт уникальную для каждого счёта информацию, записывая номер счёта и начальный баланс ( который приравнивается нулю, если при создании счёта не был задан другой баланс ). Затем увеличивается на единицу значение статического члена count, с помощью которого отслеживается количество существующих в данный момент объектов Account. Конструктор также инициализирует член node, который используется для объединения счетов в единый связанный список.

_________________

362 стр. Часть 6. Великолепная десятка

«Для одного класса существует только одна копия каждого статического объекта. К ней имеют доступ все объекты класса.»

[Помни!]

Функции accountNo( ) и accountBalance( ) служат для того, чтобы предоставлять возможность считывания номера счёта и информации о балансе из внешнего мира, но не допускать непосредственного изменения этих значений.

Функции display( ) и type( ) придают всем счетам одинаковый формат отображения. Виртуальный метод type( ) будет переопределён в классах-наследниках. Так, метод Checking::type( ) вернёт строку "Чековый". Этот распространённый трюк позволяет методам базового класса, таким как Account::display( ), использовать точное описание класса.

Подкласс Checking класса Account достаточно прост. Конструктор класса Checking только передаёт аргументы конструктору класса Account. Единственная настоящая функция-член в этом классе — это withdrawal( ), которая реализует правила работы с чековыми счетами.

Класс Savings идентичен в этом отношении классу Checking: всё, что он делает, — это реализует метод withdrawal( ).

«Любой подкласс класса Account, который не переопределяет функцию type( ), будет абстрактным, и вы не сможете создать объект этого класса.»

[Помни!]

Функции, составляющие главную программу, теперь упрощены до предела. Функция getAccount( ) создаёт счёт класса Checking или Savings ( в зависимости от символа, введённого пользователем ). Это единственное место в программе, где происходит непосредственное обращение к подклассам класса Account.

Функция displayResults( ) проходит по связанному списку, опрашивая каждый объект Account для вывода информации о чековом или сберегательном счёте ( а также о других типах счетов, если таковые встретятся в дальнейшем ). Аналогично функция process(  ) выполняет вклады и снятия со счетов ( объектов Account ). Как именно выполняются эти действия — определяют сами счета.

Метод displayResults( ) модифицирован для работы со связанным списком. В качестве аргумента этой функции передаётся связанный список, из которого функция считывает информацию. Цикл for начинается с первого объекта в списке, который возвращает вызов Node::firstNode( ). Переход к следующему объекту в списке осуществляется при помощи функции nextNode( ). Цикл завершает свою работу, когда вызов nextNode( ) возвращает 0.

Вывод на экран этой версии программы идентичен выводу предыдущей программы BUDGET2 с той разницей, что итоговая информация о счетах выводится в обратном порядке. Это связано с тем, что новые счета добавляются в начало списка.

Классы связанных списков...363

Связанный список создаётся при помощи двух классов — AccountLinkedList и Node, которые определены в заголовочном файле AccountLinkedList.h.

/* AccountLinkedList — поддерживает связанный */

/*                    список объектов Account */

#ifndef _ACCOUNTLINKEDLIST_

#define _ACCOUNTLINKEDLIST_

_________________

363 стр. Глава 31. Программа BUDGET

/* Данное предварительное объявление — неприятное следствие того, что Account не является частью пространства имён Lists. Этой неприятности мы сумеем избежать в следующей версии программы */

class Account ;

namespace Lists

{

/* Предварительное объявление классов */

class AccountLinkedList ;

class Node ;

/* LinkedList — связанный список объектов Node */

class AccountLinkedList

{

public :

AccountLinkedList( ) { pHead = 0 ; }

void addNode( Node*  pNode ) ;

Node* firstNode( ) { return pHead ; }

protected :

Node* pHead ;

} ;

/* Node — узел в связанном списке, указывающий на объект Account */

class Node

{

friend class AccountLinkedList ;

public :

Node( AccountLinkedList* pL , Account* pAcc )

{

pList = pL ;

pNext = 0 ;

pAccount = pAcc ;

pL -> addNode( this ) ;

}

static Node* firstNode( AccountLinkedList* pList )

{

return pList -> firstNode( ) ;

}

Node* nextNode( ) { return pNext ; }

Account* currentAccount( ) { return pAccount ; }

protected :

AccountLinkedList* pList ;

Node* pNext ;

Account* pAccount ;

} ;

}

#endif

_________________

364 стр. Часть 6. Великолепная десятка

Я поместил оба класса — и AccountLinkedList, и Node — в пространство имён Lists для того, чтобы отделить их от класса Account. Класс AccountLinkedList содержит только заголовочный указатель связанного списка объектов Node.

«Заголовочный указатель — это указатель на первый элемент списка.»

[Помни!]

Основная работа выполняется в классе Node. Каждый узел Node указывает на следующий в списке объект при помощи члена pNext. Кроме того, узел также указывает на объект Account при помощи указателя pAccount. Указатель pList указывает на связанный список, которому принадлежит данный узел.

Ещё раз взгляните на исходный файл BUDGET3.срр. Функция main( ) определяет объект класса AccountLinkedList — это и есть связанный список. Ссылка на него передаётся конструктору Account. Конструктор Node( ), который вызывается из конструктора Account, создаёт узел, который является членом данного связанного списка и указывает на создаваемый счёт.

Маленький исходный файл AccountLinkedList.срр нужен для того, чтобы позволить классу AccountLinkedList обратиться к члену Node. Дело в том, что класс Node определён в заголовочном файле после класса AccountLinkedList, поэтому обращаться к его членам в определении класса AccountLinkedList нельзя. Изменение порядка объявлений не решает данную проблему, поскольку класс Node в AccountLinkedList.h также содержит ссылки на класс AccountLinkedList.

/* AccountLinkedList — поддерживает связанный */

/*                   список объектов Account */

#include "AccountLinkedList.h"

namespace Lists

{

/* addNode — добавляет узел в начало текущего связанного списка */

void AccountLinkedList::addNode( Node* pNode )

{

pNode -> pNext = pHead ;

pHead = pNode ;

}

}

Оценка бюджета...365

Задача, решаемая программой BUDGET, очень проста. Тем не менее сравнение разных версий этой программы даёт вам представление об отличии чисто функционального программирования ( BUDGET1 ) от объектно-основанного ( BUDGET2 ) и объектно-ориентированного программирования ( BUDGET3 ).

Основная проблема программы BUDGET3 в том, что связанный список, который в ней реализован, в состоянии работать только с классом Account и никаким другим. Проделав такую работу по созданию связанного списка, вы, вероятно, захотите использовать его и в других программах — например, для хранения информации о студентах.

Программа BUDGET4 расширяет рамки применения класса LinkedList. Чтобы разобраться в том, как она это делает, вы должны сперва ознакомиться с материалом главы 27.

_________________

365 стр. Глава 31. Программа BUDGET

►BUDGET4...366

Программа BUDGET имитирует работу банка, позволяя вкладывать и снимать деньги со счёта. Пользователь вводит последовательность банковских счетов, причём для каждого счёта вводится серия вкладов на счёт и снятия денег со счёта. После того как будут введены все счета и все транзакции, программа выводит состояние всех счетов ( а также общий баланс по всем счетам ). В программах BUDGET2 и BUDGET3 имитируются два вида счетов — чековый и сберегательный. Чековый счёт отличается тем, что если на нём остаётся меньше 500 долларов, то за каждый обработанный чек удерживается 20 центов. Первое снятие со сберегательного счёта выполняется бесплатно, а каждое последующее обходится в 5 долларов, независимо от состояния счёта. Программа BUDGET2 использует для решения поставленной задачи функции, a BUDGET3 — объектно-ориентированные концепции из четвёртой части книги.

Кроме того, программа BUDGET3 использует для хранения счетов связанный список, что позволяет снять ограничение на количество обрабатываемых счетов, налагаемое в первых двух версиях использованием массивов. К сожалению, представленный в программе класс AccountLinkedList недостаточно гибок из-за привязки к классу Account.

Программа BUDGET4 использует шаблоны, которые позволяют убрать ограничения, состоящие в привязке списка к конкретному типу хранимых данных.

«Шаблонам посвящён материал главы 27, "Шаблоны С++".»

[Помни!]

Реализация связанного списка в виде шаблона класса...366

Приведённый далее шаблон класса LinkedList выглядит практически идентично классу AccountLinkedList из программы BUDGET3, если заменить класс Node обобщённым классом Т.

/* LinkedList — связанный список произвольных объектов */

#ifndef _ACCOUNTLINKEDLIST_

#define _ACCOUNTLINKEDLIST_

/* Предварительное объявление класса LinkedList */

template < class T > class LinkedList;

/* Node — узел связанного списка; каждый */

/*          узел указывает на объект Т */

template < class T > class Node

{

public:

Node(LinkedList< T >* pL, T* pT)

{

pList = pL;

pNext = 0;

pObject = pT;

}

Node< T >* next( ) { return pNext; }

Node<T >* next(Node< T >* pN) { pNext = pN;

return pNext; }

T* current( ) { return pObject; }

_________________

366 стр. Часть 6. Великолепная десятка

protected:

LinkedList< T >* pList;

Node< T >* pNext;

T* pObject;

};

/* LinkedList — связанный список объектов Node */

template < class T > class LinkedList

{

public :

LinkedList< T >( ) { pFirst = 0 ; }

Node< T >* firstNode( ) { return pFirst ; }

Node< T >* lastNode( )

{

/* Если список пуст, возвращает 0 */

if ( pFirst == 0 )

{

return 0 ;

}

/* В противном случае ищем последний элемент списка */

Node< T >* pN = pFirst ;

while ( true )

{

Node< T >* pNext = pN -> next( ) ;

if ( pNext == 0 )

{

break ;

}

pN = pNext ;

}

return pN ;

}

void addNode( Node< T >* pNode )

{

Node< T >* pN = lastNode( ) ;

if ( pN == 0 )

{

pFirst = pNode ;

}

else

{

pN -> next( pNode ) ;

}

}

protected :

Node< T >* pFirst ;

} ;

#endif

_________________

367 стр. Глава 31. Программа BUDGET

«Дальнейшее рассмотрение может оказаться проще, если вы мысленно замените обобщённый класс Т действительным классом Account. При этом вы увидите, насколько программа становится похожей на свою предшественницу — программу BUDGET3.»

[Советы]

Выражение template < class Т > class LinkedList представляет собой предварительное объявление шаблона, необходимое для класса Node.

«Не забывайте о том, что шаблоны классов LinkedList и Node не являются реальными классами до тех пор, пока параметр Т не будет заменён действительным классом.»

[Помни!]

Шаблон класса Node сконструирован для работы в качестве узла связанного списка. Каждый узел указывает на объект класса Т, который будет определён позже. Конструктор инициализирует члены-указатели: pList указывает на LinkedList, членом которого является данный объект Node, pObject указывает на объект типа Т, a pNext инициализируется значением 0 , указывающим, что пока узел не является членом списка.

"Активный метод" next ( Node< T >* ) добавляет текущий узел в список путём инициализации указателя pNext. "Пассивный метод" next( ) просто возвращает следующий объект Node< T > в списке. Это обычная практика кодирования, когда функция fn( ) возвращает текущее значение объекта, a fn( Т ) устанавливает его значение на основе аргумента.

Эта версия addNode( ) более усовершенствована по сравнению с представленной в программе BUDGET3, поскольку добавляет узел в конец списка. Преимущество такого подхода в том, что объекты считываются из списка в том же порядке, в котором были в него внесены, а недостаток в том, что метод lastNode( ) должен выполнять проход по всему списку всякий раз, когда требуется внести в список новый объект. Это может существенно замедлить работу программы при большом количестве элементов в списке.

Исходный код BUDGET4...368

Исходный код программы BUDGET4 практически идентичен коду BUDGET3.

/* BUDGET4.CPP — в этой версии используется */

/*                 шаблон класса LinkedList */

//

#include <cstdio>

#include <cstdlib>

#include <iostream>

using namespace std ;

#include "LinkedList.h"

/* Account — абстрактный класс, включающий */

/*          общие  свойства различных счетов */

class Account ;

template class LinkedList< Account > ;

template class Node< Account > ;

class Account

{

public :

Account::Account( LinkedList< Account >* pList ,

unsigned accNo )

{

/* Инициализация данных-членов */

_________________

368 стр. Часть 6. Великолепная десятка

accountNumber = accNo ;

balance = 0 ;

/* Внесение в список */

pNode = new Node< Account >( pList , this ) ;

pList -> addNode( pNode ) ;

count++ ;

}

/* Функции доступа */

int accountNo( ) { return accountNumber ; }

double acntBalance( ) { return balance ; }

static int noAccounts( ) { return count ; }

static Account* first( LinkedList< Account >* pLinkedList )

{

Node< Account >* pNode = pLinkedList -> firstNode( ) ;

return pNode -> current( ) ;

}

Account* next( )

{

Node< Account >* pNextNode = pNode -> next( ) ;

return pNextNode -> current( ) ;

}

/* Функции транзакций */

void deposit( double amount ) { balance += amount ; }

virtual bool withdrawal( double amount )

{

if ( balance < amount )

{

cout << "Недостаточно денег: на счету " << balance

<< ", снимаем " << amount

<< endl ;

return false ;

}

balance -= amount ;

return true ;

}

/* Функция вывода на экран */

void display( )

{

cout << type( )

<< " счёт " << accountNumber

<< " = " << balance

<< endl ;

}

virtual char* type( ) = 0 ;

protected :

Node< Account >* pNode ;

static int count ; /* Количество счетов */

unsigned accountNumber ;

double balance ;

} ;

_________________

369 стр. Глава 31. Программа BUDGET

/* Переменная для сбора статистики */

int Account::count = 0 ;

/* Checking — свойства, уникальные для чекового счёта */

class Checking : public Account

{

public :

Checking::Checking( LinkedList< Account >* pLL ,

unsigned accNo ) :

Account( pLL , accNo )

{ }

/* Перегрузка чисто виртуальных функций */

virtual bool withdrawal( double amount ) ;

virtual char* type( ) { return "Чековый" ; }

} ;

/* withdrawal — перегрузка Account::withdrawal( ) */

bool Checking::withdrawal( double amount )

{

bool success = Account::withdrawal ( amount ) ;

if ( success && balance < 500.00 )

{

balance -= 0.20 ;

}

return success ;

}

/* Savings — свойства, уникальные для сберегательного счёта */

class Savings : public Account

{

public :

Savings::Savings( LinkedList< Account >* pLL ,

unsigned accNo ) :

Account( pLL , accNo )

{ noWithdrawals = 0 ; }

/* Функции транзакций */

virtual bool withdrawal( double amount ) ;

virtual char* type( ) { return "Savings" ; }

protected :

int noWithdrawals ;

} ;

/* withdrawal — перегрузка Account::withdrawal( ) */

bool Savings::withdrawal( double amount )

{

if ( ++noWithdrawals > 1 )

{

balance -= 5.00 ;

}

return Account::withdrawal( amount ) ;

}

_________________

370 стр. Часть 6. Великолепная десятка

/* Прототипы функций */

unsigned getAccntNo( ) ;

void process( Account* pAccount ) ;

void getAccounts( LinkedList< Account >* pLinkedList ) ;

void displayResults( LinkedList< Account >* pLinkedList ) ;

/* main — собирает и выводит данные */

int main( int argcs , char* pArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

/* Создание связанного списка */

LinkedList< Account > linkedList ;

/* Чтение пользовательского ввода */

getAccounts( &linkedList ) ;

/* Вывод связанного списка */

displayResults( &linkedList ) ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

/* getAccounts — загрузка массива счетов */

void getAccounts( LinkedList< Account >* pLinkedList )

{

Account* pA ;

/* Цикл, пока не введено 'X' или 'х' */

char accountType ;         /* S or С */

while ( true )

{

cout << "Введите S для сберегательного счёта,\n"

<< "С для чекового, X для выхода: " ;

cin >> accountType ;

switch ( accountType )

{

case 'c' :

case 'C' :

pA = new Checking( pLinkedList , getAccntNo( ) ) ;

break ;

case 's' :

case 'S' :

pA = new Savings( pLinkedList , getAccntNo( ) ) ;

break ;

case 'x' :

case 'X' :

return ;

default :

cout << "Неверный ввод.\n" ;

}

_________________

371 стр. Глава 31. Программа BUDGET

/* Обработка вновь созданного объекта */

process( pA ) ;

}

}

/* displayResults — вывод информации о */

/*               счетах в связанном списке */

void displayResults( LinkedList< Account >* pLinkedList )

{

double total = 0.0 ;

cout << "\nИтоговая информация: \n" ;

for ( Node< Account >* pN = pLinkedList -> firstNode( ) ;

pN != 0 ;

pN = pN -> next( ) )

{

Account* pA = pN -> current( ) ;

pA -> display( ) ;

total += pA -> acntBalance( ) ;

}

cout << "Итого = " << total << "\n" ;

}

/* getAccntNo — номер счёта для его создания */

unsigned getAccntNo( )

{

unsigned accntNo ;

cout << "Введите номер счёта: " ;

cin >> accntNo ;

return accntNo ;

}

/* process( Account ) — обработка счёта */

void process( Account* pAccount )

{

cout << "Введите положительное число для вклада,\n"

<< "отрицательное для снятия,"

<< " 0 для завершения работы\n" ;

double transaction ;

do

{

cout << " : " ;

cin >> transaction ;

// Вклад

if ( transaction > 0 )

{

pAccount -> deposit( transaction ) ;

}

// Снятие

if ( transaction < 0 )

{

pAccount -> withdrawal( -transaction ) ;

}

} while ( transaction != 0 ) ;

}

_________________

372 стр. Часть 6. Великолепная десятка

Первые строки перед определением класса Account инстанцируют шаблоны классов LinkedList и Node в классы LinkedList< Node > и Node< Account > соответственно. Эти строки создают классы, необходимые для объединения объектов Account в связанный список.

Другая программа может с тем же успехом создать класс LinkedList< Student >, LinkedList< Name >, или другой придуманный вами класс.

Класс Account работает так же, как и в предыдущей программе BUDGET3.

Подведение итогов...373

Программа BUDGET3 использует объектно-ориентированные технологии для реализации набора классов счетов, а также включает реализацию связанного списка, который позволяет хранить неограниченное количество счетов. Единственная проблема заключается в том, что класс AccountLinkedList оказывается привязан к классу Account.

В программе BUDGET4 определён шаблон класса LinkedList< class Т >, который может работать с объектами любого типа. В этой версии единственной проблемой ( если это можно считать проблемой ) является то, что нам надо самим реализовывать связанный список.

Связанные списки используются уже очень давно, так что мы изобретаем велосипед. И хотя с методической точки зрения это совсем неплохо, в STL имеется соответствующий шаблон класса, который можно использовать в качестве контейнера ( в STL имеется масса различных контейнеров ). В следующей версии программы BUDGET мы используем этот предопределённый шаблон для хранения объектов Account.

►BUDGET5...373

Программа BUDGET5 — последняя в длинном ряду программ этой главы, призванных решать одну и ту же простую задачу имитации банковских счетов. В программе BUDGET3 эти счета представлены в виде трёх классов — Account, Savings и Checking, для хранения которых используется связанный список. Из жёстко кодированного списка для хранения объектов типа Account в программе BUDGET4 он становится шаблоном класса LinkedList< class Т >.

Программа BUDGET5 идёт на один шаг дальше и использует шаблон класса из STL.

«Для того чтобы понять эту программу, вы должны ознакомиться с главой 28, "Стандартная библиотека шаблонов".»

[Помни!]

Использование шаблона класса из STL...373

Стандартная библиотека шаблонов определяет множество различных типов контейнеров, которые можно использовать для хранения групп объектов. В следующей версии программы BUDGET используется шаблон класса list.

/* BUDGET5.CPP — идентична другим программам */

/*              Budget за исключением использования */

/*              шаблона класса из STL */

#include <cstdio>

#include <cstdlib>

#include <iostream>

#include <list>

_________________

373 стр. Глава 31. Программа BUDGET

using namespace std ;

/* Account — абстрактный класс, включающий */

/*            общие свойства различных счетов */

class Account

{

public :

Account::Account( unsigned accNo )

{

/* Инициализация данных-членов */

accountNumber = accNo ;

balance = 0 ;

count++ ;

}

/* Функции доступа */

int accountNo( ) { return accountNumber ; }

double acntBalance( ) { return balance ; }

static int noAccounts( ) { return count ; }

/* Функции транзакций */

void deposit( double amount ) { balance += amount ; }

virtual bool withdrawal( double amount )

{

if ( balance < amount )

{

cout <<"Недостаточно денег: на счету " << balance

<<", снимаем "              << amount

<< endl ;

return false ;

}

balance -= amount ;

return true ;

}

/* Функция вывода на экран */

void display( )

{

cout << type( )

<< " счёт " << accountNumber

<< " = " << balance

<< endl ;

}

virtual char* type( ) { return "Account" ; }

protected :

static int count ; /* Количество счетов */

unsigned accountNumber ;

double balance ;

} ;

/* Переменная для сбора статистики */

int Account::count = 0 ;

/* Checking — свойства, уникальные для чекового счёта */

class Checking : public Account

{

_________________

374 стр. Часть 6. Великолепная десятка

public :

Checking::Checking( unsigned accNo ) :

Account( accNo )

{ }

/* Перегрузка чисто виртуальных функций */

virtual bool withdrawal( double amount ) ;

virtual char* type( ) { return "Чековый" ; }

} ;

/* withdrawal — перегрузка Account::withdrawal( ) */

bool Checking::withdrawal( double amount )

{

bool success = Account::withdrawal( amount ) ;

if ( success && balance < 500.00 )

{

balance -= 0.20 ;

}

return success ;

}

/* Savings — свойства, уникальные для сберегательного счёта */

class Savings : public Account

{

public :

Savings::Savings( unsigned accNo ) : Account( accNo )

{ noWithdrawals = 0 ; }

/* Функции транзакций */

virtual bool withdrawal( double amount ) ;

virtual char* type( ) { return "Сберегательный" ; }

protected :

int noWithdrawals ;

} ;

/* withdrawal — перегрузка Account::withdrawal( ) */

bool Savings::withdrawal( double amount )

{

if ( ++noWithdrawals > 1 )

{

balance -= 5.00 ;

}

return Account::withdrawal( amount ) ;

}

/* AccountPtr — мы храним указатели на объекты */

/*                      Account, а не сами объекты */

typedef Account* AccountPtr ;

/* Прототипы функций */

unsigned getAccntNo( ) ;

void   process( AccountPtr pAccount ) ;

void   getAccounts( list< AccountPtr >& accList ) ;

void   displayResults( list< AccountPtr >& accList ) ;

_________________

375 стр. Глава 31. Программа BUDGET

/* main — собирает и выводит данные */

int main( int argcs , char* pArgs[ ] )

{

setlocale ( LC_ALL , ".1251" ) ; /* печать кириллицы */

/* Создание связанного списка */

list< AccountPtr > listAccounts ;

/* Чтение пользовательского ввода */

getAccounts( listAccounts ) ;

/* Вывод связанного списка */

displayResults( listAccounts ) ;

/* Пауза для того, чтобы посмотреть на результат работы программы */

system( "PAUSE" ) ; return 0 ;

}

/* getAccounts — загрузка массива счетов */

void getAccounts( list< AccountPtr >& accList )

{

AccountPtr pA ;

/* Цикл, пока не введено 'X' или 'х' */

char accountType ;          /* S or С */

while ( true )

{

cout << "Введите S для сберегательного счёта,\n"

<< "С для чекового, X для выхода: " ;

cin >> accountType ;

switch ( accountType )

{

case 'c' :

case 'C' :

pA = new Checking( getAccntNo( ) ) ;

break ;

case 's' :

case 'S' :

pA = new Savings( getAccntNo( ) ) ;

break ;

case 'x' :

case 'X' :

return ;

default :

cout << "Неверный ввод.\n" ;

}

/* Обработка вновь созданного объекта */

accList.push_back( pA ) ;

process( pA ) ;

}

}

_________________

376 стр. Часть 6. Великолепная десятка

/* displayResults — вывод информации о */

/*                счетах в связанном списке */

void displayResults( list< AccountPtr >& accntList )

{

double total = 0.0 ;

cout << "\nИтоговая информация:\n" ;

/* Создание итератора и проход по списку */

list< AccountPtr >::iterator iter ;

iter = accntList.begin( ) ;

while ( iter != accntList.end( ) )

{

AccountPtr pAccount = *iter ;

iter++ ;

pAccount -> display( ) ;

total += pAccount -> acntBalance( ) ;

}

cout << "Итого = " << total << endl ;

}

/* getAccntNo — номер счёта для его создания */

unsigned getAccntNo( )

{

unsigned accntNo ;

cout << "Введите номер счёта: " ;

cin >> accntNo ;

return accntNo ;

}

/* process( Account ) — обработка счёта */

void process( AccountPtr pAccount )

{

cout << "Введите положительное число для вклада,\n"

<< "отрицательное для снятия,"

<< "0 для завершения работы\n" ;

double transaction ;

do

{

cout << ":" ;

cin >> transaction ;

// Вклад

if ( transaction > 0 )

{

pAccount -> deposit( transaction ) ;

}

// Снятие

if ( transaction < 0 )

{

pAccount -> withdrawal( -transaction ) ;

}

} while ( transaction != 0 ) ;

}

Заголовочный файл list содержит определение шаблона класса list из STL. Классы Account, Checking и Savings остаются неизменными ( т.е. такими, как в программе BUDGET3 ). Изменения начинаются с определения типа AccountPtr примерно в середине программы.

_________________

377 стр. Глава 31. Программа BUDGET

Создание списка счетов...378

Функция main( ) создаёт список объектов listAccounts, который имеет тип list< AccountPtr >.

Теоретически я могу реализовать шаблон класса как list< Account* >, но так поступают редко — дабы не портить определения внутри шаблонов классов STL, обычно используют синонимы указателей, полученные при помощи оператора typedef.

«Тип AccountPtr определён с использованием ключевого слова typedef и представляет собой то же, что и Account*. Таким образом, везде, где написано AccountPtr, можно читать "указатель на Account".»

[Помни!]

Функция main( ) передаёт список указателей на объекты Account функциям getAccounts( ) и displayResults( ). Метод getAccounts( ) добавляет объекты Account в конец списка при помощи функции-члена push_back( ).

Функция displayResults( ) может удалять объекты Account из списка при помощи одного из предназначенных для этой цели методов; однако это будет так называемое деструктивное чтение, которое изменяет список ( в нашем случае — удаляя из него объекты ). Поскольку мы хотим иметь возможность работать со списком и после вывода его на экран, нам надо воспользоваться итератором — объектом, который указывает на объекты в списке. Программа в цикле перемещает итератор от одного элемента списка к следующему.

Функция displayResults( ) определяет итератор iter в начале цикла while( ). Присвоение iter = accntList.begin( ) инициализирует объект iter первым элементом списка. Значение accntList.end( ) представляет собой "объект, непосредственно следующий за последним объектом в контейнере". Таким образом, цикл должен полностью обойти весь список к моменту, когда iter становится равным accntList.end( ). Выражение *iter даёт нам то, что можно назвать "текущим объектом", а выражение iter++ перемещает итератор к следующему объекту в списке.

В остальном программа BUDGET5 эквивалентна программам BUDGET4 и BUDGET3.

_________________

378 стр. Часть 6. Великолепная десятка

Приложение. Содержимое прилагаемого компакт-диска

На прилагаемом компакт-диске вы найдёте все полные исходные тексты программ, использованных в данной книге. В папке CPP_Programs тексты находятся в папках, соответствующих главам книги. Для удобства работы вам следует скопировать всю папку CPP_Programs к себе на жёсткий диск и работать с программами с жёсткого диска, что позволит вам редактировать программы, внося в них различные изменения.

Кроме того, в папке BUDGET имеется демонстрационное финансовое приложение. Здесь вы найдёте пять версий этого приложения — от простейшего к наиболее сложному, с использованием большого количества различных возможностей С++.

Если у вас не установлено программное обеспечение для работы с pdf-файлами, то вы можете воспользоваться Acrobat Reader, который находится на прилагаемом компакт-диске в одноименном каталоге.

На компакт-диске вы найдёте и описанную в книге среду программирования Dev-C++. Её инсталляционный файл devcpp4980.ехе, который надо запустить для установки Dev-C++ на ваш компьютер, находится в папке devcpp.

Кроме того, здесь же имеется документация ( на английском языке ) по стандартной библиотеке шаблонов ( STL ).

_________________

379 стр. Приложение

ПИКТОГРАММЫ

Технические подробности, которые можно пропустить при первом чтении.

ОГЛАВЛЕНИЕ

Часть 1. Первое знакомство с С++

Глава 1. Написание вашей первой программы 25

►Как пишут программы...26

Глава 4. Выполнение логических операций 55

►Использование простых логических операторов...55

Логические операции и действительные переменные...58

►Бинарные числа в С++...60

Глава 5. Операторы управления программой 66

►Выполнение циклов...68

Избегайте бесконечных циклов...73

Часть 3. Введение в классы 143

Глава 13. Работа с классами 154

►Добавление функции-члена...156

Создание функции-члена...156

►Вызов функций-членов...157

Именование текущего объекта...160

►Определение функции-члена...162

Глава 14. Указатели на объекты...167

►Зачем, использовать указатели и ссылки...174

Глава 15. Защищённые члены класса: не беспокоить!...181

►Защищённые члены...181

Как устроены защищённые члены...182

Глава 16. Создание и удаление объектов...188

►Создание объектов...188

Глава 19. Статические члены...224

►Определение статических членов...224

Обращение к статическим данным-членам...226

Часть 4. Наследование...231

Глава 21. Знакомство с виртуальными функциями-членами: настоящие ли они...240

►Как работает полиморфизм...245

Глава 22. Разложение классов 249

►Реализация абстрактных классов...253

Концепция абстрактных классов...254

►Разделение исходного кода С++...259

Реализация класса student...261

Советы, которые помогут сохранить много времени и усилий.

ОГЛАВЛЕНИЕ

Часть 1. ПЕРВОЕ ЗНАКОМСТВО с С++...23

Глава 1. Написание вашей первой программы...25

►Постигая концепции С++...25

Инсталляция Dev-C++...27

►Создание первой программы...31

Введение кода1...32

Введение кода2...33

Введение кода3...33

Введение кода4...34

Почему С++ так требователен...35

►Разбор программ...36

Использование инструкций в программах...38

Объявления1...38

Объявления2...38

Глава 2. Премудрости объявления переменных...41

► Объявление разных типов переменных 42

Ограничения, налагаемые на целые числа в С++...43

Ограничения диапазона1...43

Ограничения диапазона2...43

►Объявления типов переменных...45

►Логические выражения...48

►Выражения смешанного типа...48

Соглашения по именованию...49

Глава 3. Выполнение математических операций...50

►Использование операторов присвоения1...54

►Использование операторов присвоения2...54

Глава 4. Выполнение логических операций...55

►Использование простых логических операторов...55

Хранение логических значений1...57

Хранение логических значений2...57

►Выполнение побитовых логических операций...62

Глава 5. Операторы управления программой...66

►Выполнение циклов...68

Использование цикла for...71

Специальные операторы управления циклом...73

►Инструкция выбора...77

Часть 2. Становимся функциональными программистами...79

Глава 6. Создание функций...81

►Написание и использование функций...81

Разделяй и властвуй 83

►Подробный анализ функций...84

Простые функции1...85

Простые функции2...85

Глава 7. Хранение последовательностей в массива...92

Преимущества массивов 92

Работа с массивами...93

►Управление строками...100

►Тип string...103

Глава 8. Первое знакомство с указателями в С++...105

►Размер переменной...105

►Адресные операторы...106

►Использование кучи...113

Использование блока памяти...115

Глава 9.  Второе знакомство с указателями...117

►Операции с указателями...117

Почему при работе со строками пользуются указателями...122

Отличия между указателями и массивами...122

►Объявление и использование массивов указателей...124

Использование массивов строк...124

Глава 10. Отладка программ на С++...128

►Использование отладочной печати...128

Выявление "жучка" № 1...130

►Использование отладчика...134

Пошаговое выполнение программы1...136

Пошаговое выполнение программы2...136

Пошаговое выполнение программы3...136

Часть 3. Введение в классы...143

Глава 12. Классы в С++...149

►Формат класса...149

Глава 13. Работа с классами...154

►Добавление функции-члена...156

Создание функции-члена...156

Именование членов класса1...157

Именование членов класса2...157

►Определение функций-членов вне класса...164

Глава 14. Указатели на объекты...167

►Передача объектов функциям...171

Передача объекта no ссылке...173

►Сравнение указателей и ссылок...175

►Использование связанных списков...176

Программа LinkedListData...178

Глава 15. Защищённые члены класса: не беспокоить!...181

►Защищённые члены...181

Как устроены защищённые члены...182

Глава 16. Создание и удаление объектов...88

►Использование конструкторов...189

Работа с конструкторами...190

Конструирование одного объекта...190

Глава 17. Аргументация конструирования...198

►Конструирование членов класса...204

Конструкторы константных членов...208

►Управление последовательностью конструирования...208

Порядок создания глобальных объектов не определён...210

Глава 19. Статические члены 224

►Определение статических членов...224

Зачем нужны статические члены...224

Использование статических членов...225

Часть 4. Наследование...231

Глава 20. Наследование классов...233

►Как наследуется класс...234

Использование подкласса...236

Глава 21. Знакомство с виртуальными функциями-членами: настоящие ли они...240

►Зачем нужен полиморфизм...243

►Как работает полиморфизм...245

Глава 22. Разложение классов...249

►Разделение исходного кода С++...259

Разделение программы - класс student...260

Определение пространства имён...261

Реализация приложения...263

Создание файла проекта в Dev-C++...265

Часть 5. Полезные особенности...269

Глава 23. Оператор присвоения...271

►Мелкое копирование — глубокие проблемы...272

►Защита от копирования...276

Глава 24. Использование потоков ввода-вывода...277

►Знакомство с подклассами fstream1...278

►Знакомство с подклассами fstream2...278

►Знакомство с подклассами fstream3...278

►Знакомство с подклассами fstream4...278

►Прямое чтение из потока...282

►Подклассы strstream...285

Глава 25. Обработка ошибок и исключения...290

►Так что же мы будем бросать?...295

Глава 26. Множественное наследование...298

►Виртуальное наследование...301

Глава 27. Шаблоны С++...308

►Обобщение функции в шаблон...309

►Зачем нужны шаблоны классов1...314

►Зачем нужны шаблоны классов2...314

Глава 28. Стандартная библиотека шаблонов...317

►Контейнер string...317

Часть 6. Великолепная десятка...329

Глава 31. Программа BUDGET...343

►BUDGET4...366

Реализация связанного списка в виде шаблона класса...366

Диск. Материал на прилагаемом компакт-диске.

ОГЛАВЛЕНИЕ

Часть 1. Первое знакомство с С++...23

Глава 1. Написание вашей первой программы...25

►Как пишут программы...26

►Разбор программ...36

Определение структуры программ С++...37

Глава 2. Премудрости объявления переменных...41

►Объяление разных типов переменных...42

Решение проблемы усечения дробной части...44

Глава 5. Операторы управления программой...66

►Выполнение циклов...68

Специальные операторы управления циклом...73

Часть 2. Становимся функциональными программистами...79

Глава 10. Отладка программ на С++...128

►Использование отладочной печати...128

Выявление "жучка" № 2...131

►Использование отладчика...134

Запуск тестовой программы...135

Часть 3. Введение в классы...143

Глава 17. Аргументация конструирования...198

►Конструирование членов класса1...204

►Конструирование членов класса2...204

Глава 18. Копирующий конструктор...213

►Автоматический конструктор копирования...215

►"Мелкие " и "глубокие " копии...217disk219str

Глава 19. Статические члены...224

►Оъявление статических функций-членов...228

Часть 4. Наследование...231

Глава 20. Наследование классов...233

►Зачем, нужно наследование...234

Глава 21. Знакомство с виртуальными функциями-членами: настоящие ли они 240

►Как работает полиморфизм...245

Глава 22. Разложение классов...249

►Разложение...249

Часть 5. Полезные особенности...269

Глава 24. Использование потоков ввода-вывода...277

►Как работают потоки ввода-вывода...277

Глава 26. Множественное наследование...298

►Виртуальное наследование...301

Запомните — это важно.

ОГЛАВЛЕНИЕ

Часть 2. Становимся функциональными программистами...79

Глава 6. Создание функций...81

►Перегрузка функций...88

Глава 7. Хранение последовательностей в массивах...92

►Использование символьных массивов...98

Создание строки символов1...99

Создание строки символов2...99

Создание строки символов3...99

►Управление строками...100

Глава 9. Второе знакомство с указателями...117

►Операции с указателями...117

Использование операций над указателями для адресации внутри массива...119

Использование указателей для работы со строками...120

Использование указателей для работы со строками...120

►Объявление и использование массивов указателей...124

Использование массивов строк...124

Глава 10. Отладка программ на С++...128

►Использование отладочной печати...128

►Использование отладчика...134

Пошаговое выполнение программы...136

Часть 3. Введение в классы...143

Глава 11. Знакомство с объектно-ориентированным программированием...145

►Классификация, микроволновых печей...146

Глава 12. Классы в С++ 149

►Формат класса...149

►Обращение к членам класса...150

Глава 13. Работа с классами 154

►Вызов функций-членов...157

Доступ к членам из функции-члена...159

►Разрешение области видимости1...161

►Разрешение области видимости2...161

►Определение функции-члена...162

Глава 14. Указатели на объекты 167

►Определение массивов и указателей...167

►Передача объектов функциям...171

Вызов функции с передачей объекта по значению...171

►Возврат к куче1...175

►Возврат к куче2...175

►Использование связанных списков...176

Программа LinkedListData...178

Глава 15. Защищённые члены класса: не беспокоить!...181

►Защищённые члены...181

►Обращение к защищённым члена...184

Глава 16. Создание и удаление объектов...188

►Создание объектов1...188

►Создание объектов2...188

►Использование конструкторов...189

Конструирование составных объектов...192

►Что такое деструктор...194

Работа с деструкторами...194

Глава 17. Аргументация конструирования...198

►Перегрузка конструктора1...200

►Перегрузка конструктора2...200

►Управление последовательностью конструирования...208

Глава 18. Копирующий конструктор...213

►Копирование объекта...213

Зачем нужен копирующий конструктор...213

Глава 19. Статические члены...224

►Определение статических членов...224

Использование статических членов...225

Глава 21. Знакомство с виртуальными функциями-членами: настоящие ли они...240

Часть 4. Наследование...231

Глава 22. Разложение классов...249

►Реализация абстрактных классов...253

Создание полноценного класса из абстрактного...255

Нужны ли чисто виртуальные функции...257

►Разделение исходного кода С++...259

Часть 5. Полезные особенности...269

Глава 28. Стандартная библиотека шаблонов...317

►Контейнер string...317

Часть 6. Великолепная десятка...329

Глава 31. Программа BUDGET...343

►BUDGET3...355

Реализация модуля со связанным списком...356

Работа со счетами1...358

Работа со счетами2...358

Классы связанных списков...363

►BUDGET4...366

Реализация связанного списка в виде шаблона класса...366

►BUDGET5...373

Создание списка счетов...378

Атас! Тоже важное напоминание. Это указание о том, что здесь легко допустить ошибку и даже не догадаться о ней.

ОГЛАВЛЕНИЕ

Часть 1. Первое знакомство с С++

Глава 1. Написание вашей первой программы...25

►Как пишут программы...26

►Как пишут программы...26

►Инсталляция Dev-C++...27

►Инсталляция Dev-C++...27

►Разбор программ...36

Использование в исходном коде комментариев...37

►Вычисление выражений...39

Сохранение результатов выражения...39

Глава 2. Премудрости объявления переменных...41

►Объяление разных типов переменных...42

Решение проблемы усечения дробной части...44

Ограничения, налагаемые на числа с плавающей точкой...44

Ограниченность диапазона...45

►Объявления типов переменных...45

Константы...47

►Выражения смешанного типа...48

Глава 3. Выполнение математических операций...50

►Бинарная арифметика...50

Глава 4. Выполнение логических операций...55

►Использование простых логических операторов...55

Глава 5. Операторы управления программой...66

►Управление ходом программы с помощью команд ветвления...66

►Выполнение циклов...68

Цикл while...68

Использование цикла for...71

Часть 2. Становимся функциональными программистами...79

Глава 6. Создание функций...81

►Перегрузка функций...88

►Использование заголовочных файлов...91

Глава 7. Хранение последовательностей в массивах...92

►Преимущества массивов...92

Работа с массивами...93

Выход за границы массива...97

►Управление строками1...100

►Управление строками2...100

►Управление строками3...100

►Тип string...103

Глава 8. Первое знакомство с указателями в С++...105

►Адресные операторы...106

►Передача указателей функциям...111

Передача аргументов по значению...112

►Использование кучи...113

Проблемы области видимости...114

Глава 9. Второе знакомство с указателями...117

►Операции с указателями...117

Повторное знакомство с массивами в свете указателей...118

Часть 3. Введение в классы...143

Глава 14. Указатели на объекты...167

►Зачем, использовать указатели и ссылки...174

►Использование связанных списков...176

Глава 16. Создание и удаление объектов...188

►Использование конструкторов...189

Работа с конструкторами...190

Конструирование одного объекта...190

Глава 17. Аргументация конструирования...198

►Как снабдить конструктор аргументами...198

Как использовать конструктор с аргументами...199

►Определение конструкторов по умолчанию...203

►Управление последовательностью конструирования...208

Все глобальные объекты создаются до вызова main( )...210

Глава 18. Копирующий конструктор...213

►"Мелкие " и "глубокие " копии...217

Часть 4. Наследование 231

Глава 21. Знакомство с виртуальными функциями-членами: настоящие ли они 240

►Виртуальные особенности...247

Глава 22. Разложение классов...249

►Разложение...249

►Разделение исходного кода С++...259

Разделение программы - класс GraduateStudent1...262

Разделение программы - класс GraduateStudent2...262

Часть 5. Полезные особенности...269

Глава 24. Использование потоков ввода-вывода...277

►Работа с манипуляторами1...287

►Работа с манипуляторами2...287

Глава 27. Шаблоны С++...308

►Обобщение функции в шаблон1...309

►Обобщение функции в шаблон2...309

Глава 28. Стандартная библиотека шаблонов 317

►Итераторы...321

Часть 6. Великолепная десятка...329

Глава 31. Программа BUDGET...343

►BUDGET2...348

Предметный указатель

ОГЛАВЛЕНИЕ

Символы

+, ++, -, 50

%, %=, --, /, 51

+=, -=, *, *=, 51

--, ++, 53

!, &&, ||, <, >, 56

<=, ==, >=, !=, 56

&, ^, |, ~, 62

С

cin, cout, 39

cin, cout, 278

class, 150

Corder Mary, 16

D

delete, 115

Dev-C++, 27

Ввод программы, 32

Инсталляция, 27

Компиляция, 34

Настройка и особенности, 336 - 42

Отладка, 134

Проект, 265

Просмотр функций, 95

Указание аргументов, 127

F

friend, 184

I

inline, 163

L

list, 320

M

mар, 324

Moore, Claudette, 16

N

new, 115

P

private, 183

protected, 182

S

size_t, 319

STL, 317

struct, 150

T

template, 310

this, 160

Thomas, Suzanne, 16

typedef, 315

U

unsigned int, 46

V

virtual, 245

_________________

380 стр.  Предметный указатель

А

Абстракция, 145 , 182

Адрес, 106

Ассоциативный массив, 324

Б

Базовый класс, 234

Байт, Бит, 61

В

Виртуальная функция, 245 , 246

Чисто виртуальная функция, 254

Виртуальное наследование, 303

Временные объекты, 221

Выражение, 39 , 51

Смешанного типа, 48

Г

Глубокое копирование, 221

Д

Данное-член, 155

Двоичная система счисления, 60

Действительные числа в С++, 44

Декремент, 53

Деструктор, 194 , 206

Виртуальный, 247

3

Защищённые члены, 181

И

Инкапсуляция, 259

Инкремент, 53

Исключение, 290

Исключительная ситуация, 290

Итератор, 321

К

Класс, 149 , 188

Абстрактный, 254

Базовый, 234

Друзья, 184

Защищённые члены, 181

Конкретный, 254

Статические члены, 224

Классификация, 146

Комментарии, 37

Компилятор, 26

Константа, 47

Конструктор, 189 , 198

Аргументы, 198

Копирования, 213

Копирования по умолчанию, 215

Перегрузка, 200

По умолчанию, 203 , 206

Порядок вызова, 208

Члена класса, 204

Контейнер, 317

Кордер, Мэри, 16

Куча, 113 , 175

М

Массив, 92 , 118 , 167

Ассоциативный, 324

Индекс, 93 , 95 , 167

Инициализация, 96

Объектов, 168

Объявление, 93

Символьный, 98

Мелкое копирование, 219

Метод, 157

Множественное наследование, 298 — 307

Конструирование объектов, 306

Модуль, 89 , 259

Мур, Клодетт, 16

Н

Наследование, 233 — 39

Виртуальное, 303

Множественное, 298 — 307

О

Область видимости, 113

Разрешение, 161

Объект, 149 , 188

Активизация, 154

Временный, 221

Глобальный, 188

Локальный, 188

Текущий, 160

_________________

381 стр. Предметный указатель

Объектно-ориентированное

программирование, 18 , 145 , 243

Объявление, 38

Оператор, 39

+, ++, -, 50

%, %=, --, /, 51

+=, -=, *, *=, 51

--, ++, 53

!, &&, ||, <, >, 56

<=, ==, >=, !=, 56

&, ^, |, ~, 62

break, 74

continue, 75

delete, 115

new, 115

return, 85

sizeof, 106

Бинарный, 50

Разрешения области видимости, 161

Операторы потоков, 277

Операция, 50

Логическая, 55

Над указателями, 117

Побитовая, 62

Порядок выполнения, 52

Унарная, 53

Операция условного перехода, 66

Отладка, 128 — 41

Отладчик, 134 — 41

Отношение СОДЕРЖИТ, 238

Отношение ЯВЛЯЕТСЯ, 234 , 249

Ошибка округления, 59

Ошибки времени исполнения, 128

Ошибки компиляции, 128

П

Перегрузка функций, 88

Передача аргументов по значению, 112

Передача аргументов по ссылке, 113

Переменная, 38 , 41

Глобальная, 90

Инициализация, 46

Локальная, 90

Размер, 105

Статическая, 90 , 209

Переопределение функций, 243

Позднее связывание, 243

Полиморфизм, 243

Потоки ввода-вывода, 277 — 89

Манипуляторы, 288

Стандартные, 278

Предварительное описание, 186

Приведение

Повышающее, 48

Понижающее, 48

Приоритет, 52 , 118 , 170

Присвоение, 39 , 54

Программа, 26

Пространство имён, 259 , 261

Профайлер, 340

Р

Разворачивание стека, 293

Разложение, 249 , 252

Разыменование, 169

Раннее связывание, 243 , 245

Реализация, 150

С

Свойство класса, 150

Связанный список, 176

Семантика, 26

Сигнатура, 89

Синтаксис, 26

Смещение, 117

Соглашения по именованию, 49

Сокращённые вычисления, 59

Стандартная библиотека шаблонов, 317

Статические члены, 224

Стек вызова, 174

Строка, 100 , 120

Т

Тип

bool, 46

char, 46

_________________

382 стр. Предметный указатель

double, 46

float, 46

int, 46

long, 46

string, 46 , 103

unsigned long, 46

void, 84

Томас, Сьюзанна, 16

У

Указатель, 105 , 168

Уровень абстракции, 145

Утечка памяти, 263

Ф

Функция, 81 , 171

Аргументы, 84 , 85

Возвращаемое значение, 84

Перегрузка, 88

Передача аргументов по значению, 112

Передача аргументов по ссылке, 113

Прототип, 90

Сигнатура, 89

Тело, 83

Функция-член, 155

Встраиваемая, 163

Ц

Целые числа в С++, 43

Цикл, 68

do..while, 69

for, 71

while, 68

Бесконечный, 73

Вложенный, 76

Оператор break, 74

Оператор continue, 75

Ч

Чисто виртуальная функция, 254

Член класса, 150 , 225

Член объекта, 225

Ш

Шаблон, 309

Инстанцирование, 310

Класса, 311

Э

Экземпляр, 147 , 150

_________________

383 стр. Предметный указатель

ТИПОГРАФИЯ

Научно-популярное издание

Стефан Р. Дэвис

С++ для "чайников"

5-е издание

Литературный редактор П.Н. Мачуга

Верстка  А.Н. Полинчик

Художественный редактор В.Г. Павлютин

Корректоры З.В. Александрова,

Л.B. Чернокозинская

Издательский дом "Вильямс"

127055, г. Москва, ул. Лесная, д. 43, стр. 1

Подписано в печать 13.08.2007. Формат 70x100/16.

Гарнитура Times.Печать офсетная.

Усл. печ. л. 30,9. Уч.-изд. л. 18,1.

Тираж 3000 экз. Заказ № 2482.

Отпечатано по технологии CtP

в ОАО "Печатный двор" им. А. М. Горького

197110 , Санкт-Петербург, Чкаловский пр., 15.

Пожертвование и заработок в партнёрских программах

Вы можете переслать любую сумму копирайтеру за переделку книги в ФБ2 (FB2)  формат, на любой из нижеприведённых WEBMONEY кошельков:

Z587187844833  ; B889951872392 ; R184950127691 ; U227750651065

или перечислив деньги на счёт:

ОАО "АСБ БЕЛАРУСБАНК" г. Минск

филиал 527 "Белжердор"

лицевой счёт №37001444

Заработай на файлообменнике DEPOSITFILES:

Заработай на файлообменнике TURBOBIT:

Заработай на файлообменнике LETITBIT:

Ошибки

стр.45

Тип данных double также ограничен, хотя его диапазон намного обширнее диапазона целочисленных переменных. Максимальным значением типа int является число чуть больше 2 млрд.; максимальное значение переменной типа double приблизительно равно 10^308, т.е. 1 с 308 нулями[7].

В английской версии книги ( стр.32 ) написанно, что максимальное значение переменной типа double приблизительно равно 10^38, т.е. 1 с 38 нулями[7].

===============

Чтобы программа LayoutError-рус.ехе (стр.110) выдала нужное нам сообщение нужно в программу дописать:

cout << "&upper = 0x" << &upper<< "\n" ;

cout << "&n      = 0x" << &n  << "\n" ;

cout << "&lower  = 0x" << &lower << "\n" ;

Иначе на консоли будет не то что нужно.

===============

стр.214 в программах ( в книге и исходниках )

/* CopyConstructor — работа конструктора копирования */

стр.216

/* DefaultCopyConstructor — демонстрация вызова */

стр.261

/* Student — реализация методов класса Student */

стр. 262

/* GraduateStudent — специальный тип Student */

нету #include <strings.h>

( или  #include <string.h> )

из-за чего они не компилируются  в dev-c++

==================

DemoAssignmentOperator

стр.273 надо

#include <strings.h>

вместо

#include <string>

====================

void fn( Name& n )  пробел между & и  n

стр. 276

====================

На 164 стр. вместо комментариев

/* SavingsClassInline — вызов фукции-члена, */

/*               объявленной и определённой */

/*                    в классе Savings */

должно быть

/* SavingsClassOutline — вызов фукции-члена, */

/*               объявленной   в классе Savings ( заголовочном файле ), но определённой */

/*               в программе    SavingsClassOutline или  */

/*      тело функции находится в отдельном исходном файле */

=======================

На 168 стр. оригинала ошибки текста программы ArrayOfStudents

Students[ 10 ] а надо Student s[ 10 ]. Пропущен пробел между t и s.

==================

d1 = 2.0 ; || Значение этого выражения равно 2.0

стр.275 вместо наклонных // вертикальные ||

=================

в программе   /* DefaultStreamOutput */

нету    #include <cstdlib >

стр. 278

=================

проверить

/* delete pName ; */

218 стр. Часть 3. Введение в классы

=====================

стр. 281

в программе /* StreamInput — ВВОД ДАННЫХ С ИСПОЛЬЗОВАНИЕМ fstream */

нету    #include <cstdlib >

из-за чего она не компилируется  dev-c++

================

стр. 283

в программе

/* FileInput — чтение блока данных из файла */

нету    #include <cstdlib >

из-за чего она не компилируется  dev-c++

================

300 стр. Часть 5 нет закрывающей фигурной скобки

============

304 стр. вместо

Bed( int weight ) : Furniture( weight ) { }

нужно

Bed() { }

===============

304 стр. вместо

Sofa( int weight ) : Furniture( weight ) { }

нужно

Sofa( ){ }

===============

311 стр.

написано

#include <string>

а надо

#include <string.h>

или

#include <strings.h>

================

340 стр. Часть 6. Великолепная десятка

void fn1( Person&p )

===============

функция width( n ) либо манипулятор setw( с )

288 стр. Часть 5. Полезные особенности

========================

в программе /* CallStaticMember */ стр.228 нет

#include <strings.h>

из-за чего она не компилируется  dev-c++

===================================

Примечания

1

Как правило, но, вообще говоря, это выполняется не всегда.

2

В С++ отнюдь не запрещается использование символов в верхнем регистре — просто язык чувствителен к регистру, а это значит, что int main и Int Main, например, означают совершенно разные вещи.

3

К сожалению, недостаточно полная, и только на английском языке.

4

Имя может также начинаться с символа подчёркивания, хотя на практике это используется довольно редко.

5

Эта величина опять-таки существенно зависит от типа разрабатываемой программы.

6

Вообще говоря, диапазон значений типа int определяется множеством факторов — в первую очередь компилятором, на выбор типа int которого оказывает огромное влияние тип компьютера, поэтому считать определённым раз и навсегда, что диапазон значений int простирается от -2^32 до +232-1, нельзя.

7

Это не означает, будто тип double может представить 10^38 разных значений; вспомните, что говорилось выше о количестве разрядов в числах этого типа.

8

В качестве ещё одной ремарки: операторы сравнения вообще достаточно взаимозаменяемы. Так, например, ( a == b ) эквивалентно ( ! ( a>b )&&! ( a<b ) ).

9

Более того, в данном случае это не красивое слово, а строгий математический термин.

10

Что и было сделано у некоторых народов, например у майя или чукчей.

11

Увеличение в данном случае — достаточно условное название. Чаще всего здесь действительно выполняется увеличение счётчика цикла, однако это может быть любая инструкция С++, в том числе и пустая.

12

Более того, как вы узнаете позже, тела функции в данном модуле может и не оказаться.

13

Размер указателя зависит не только от типа процессора, но и от операционной системы, используемого компилятора и так называемой модели памяти создаваемой программы.

14

Это сделано некорректно; как минимум член valFriend не может быть определён в классе того же типа, не считая массы других ошибок. Поэтому к данному примеру следует относиться как к не более чем поясняющей сугубо теоретической модели, которая никогда не будет даже скомпилирована.

15

Вообще говоря, это зависит от используемого компилятора. Так, тот же Dev-C++ вызовет данную функцию, в чём легко убедиться, скомпилировав и выполнив приведённый пример ( дополнив его, естественно, функцией main( ), в которой вызывается fn( ) ).

16

Ещё одно замечание: в этом случае вы должны позаботиться о том, чтобы счётчик увеличивался во всех конструкторах, включая конструктор копирования.

17

Вообще говоря, чисто виртуальная функция может иметь тело, но обсуждение этого вопроса выходит за рамки данной книги.

18

В определении тела защищённых конструктора копирования и оператора присвоения нет необходимости, поскольку они никогда не будут вызываться. Таким образом, вы можете просто указать их в защищённой части объявления класса, никак их не реализуя.

Content

Дэвис Стефан Рэнди С++ для "чайников" .

С++ для "чайников". Дэвис Стефан Р.

ОГЛАВЛЕНИЕ...5

СОДЕРЖАНИЕ...6

ВВЕДЕНИЕ...17

Часть 1. ПЕРВОЕ ЗНАКОМСТВО с С++...23

Глава 1. НАПИСАНИЕ ВАШЕЙ ПЕРВОЙ ПРОГРАММЫ...25

►Постигая концепции С++...25

►Что такое программа...26

►Как пишут программы...26

►Инсталляция Dev-C++...27

Настройка русского языка

Как настроить русский язык в консольных программах?

Настройка Dev-C++...30

СОВЕТ ДНЯ

►Создание первой программы...31

Введение кода...32

Построение вашей программы...34

Почему С++ так требователен...35

►Выполнение программы...36

Dev-C++ — это не Windows

Помощь в Dev-C++

►Разбор программ...36

Определение структуры программ С++...37

Использование в исходном коде комментариев...37

Использование инструкций в программах...38

Объявления...38

Генерирование вывода...39

►Вычисление выражений...39

Сохранение результатов выражения...39

Обзор программы Convert продолжается...40

Глава 2. ПРЕМУДРОСТИ ОБЪЯВЛЕНИЯ ПЕРЕМЕННЫХ...41

►Объяление переменных...41

►Объяление разных типов переменных...42

Ограничения, налагаемые на целые числа в С++...43

Округление до целых значений...43

Ограничения диапазона...43

Решение проблемы усечения дробной части...44

Ограничения, налагаемые на числа с плавающей точкой...44

Перечисление...44

Скорость вычислений...45

Потеря точности...45

Ограниченность диапазона...45

►Объявления типов переменных...45

Константы...47

Специальные символы...47

Коллизии между С++ и именами файлов MS DOS...48

►Логические выражения...48

►Выражения смешанного типа...48

Соглашения по именованию

Глава 3. ВЫПОЛНЕНИЕ МАТЕМАТИЧЕСКИХ ОПЕРАЦИЙ...50

►Бинарная арифметика...50

►Анализ выражений...51

►Определение порядка операций...52

►Выполнение унарных операций...53

Почему так важен оператор инкремента...53

►Использование операторов присвоения...54

Глава 4. ВЫПОЛНЕНИЕ ЛОГИЧЕСКИХ ОПЕРАЦИЙ...55

►Зачем нужны логические операторы...55

►Использование простых логических операторов...55

Хранение логических значений...57

Использование целых переменных в качестве логических...58

Логические операции и действительные переменные...58

Сокращённые вычисления в С++...59

►Бинарные числа в С++...60

Десятичная система счисления...60

Другие системы счисления...60

Двоичная система счисления...60

Выражения с римскими числами...61

►Выполнение побитовых логических операций...62

Побитовые операции с одним битом...62

Использование побитовых операторов...63

Простой пример...64

Практическое применение логических вычислений...65

Глава 5. ОПЕРАТОРЫ УПРАВЛЕНИЯ ПРОГРАММОЙ...66

►Управление ходом программы с помощью команд ветвления...66

►Выполнение циклов...68

Цикл while...68

Использование операторов инкремента и декремента...70

Использование цикла for...71

Избегайте бесконечных циклов...73

Специальные операторы управления циклом...73

►Вложенные команды управления...76

►Инструкция выбора...77

Часть 2. СТАНОВИМСЯ ФУНКЦИОНАЛЬНЫМИ ПРОГРАММИСТАМИ...79

Глава 6. СОЗДАНИЕ ФУНКЦИЙ...81

►Написание и использование функций...81

Определение функции sumSequence( )...83

Вызов функции sumSequence( )...83

Разделяй и властвуй...83

►Подробный анализ функций...84

Простые функции...85

Функции с аргументами...85

Функции с одним аргументом...85

Функции с несколькими аргументами...87

Функция main( )...87

►Перегрузка функций...88

►Определение прототипов функций...89

►Хранение переменных в памяти...90

►Использование заголовочных файлов...91

Глава 7. ХРАНЕНИЕ ПОСЛЕДОВАТЕЛЬНОСТЕЙ В МАССИВАХ...92

►Преимущества массивов...92

Работа с массивами...93

Инициализация массива...96

Выход за границы массива...97

Использовать ли массивы...97

Определение и использование массивов с элементами-массивами...98

►Использование символьных массивов...98

Создание строки символов...99

►Управление строками...100

►Тип string...103

Глава 8. ПЕРВОЕ ЗНАКОМСТВО С УКАЗАТЕЛЯМИ В С++...105

►Размер переменной...105

►Что такое адрес...106

►Адресные операторы...106

►Использование указателей...108

Сравнение указателей и почтовых адресов...109

Использование разных типов указателей...109

►Передача указателей функциям...111

Передача аргументов по значению...112

Передача значений указателей...112

Передача аргументов по ссылке...113

►Использование кучи...113

Область видимости...113

Проблемы области видимости...114

Использование блока памяти...115

Глава 9. ВТОРОЕ ЗНАКОМСТВО С УКАЗАТЕЛЯМИ...117

►Операции с указателями...117

Повторное знакомство с массивами в свете указателей...118

Использование операций над указателями для адресации внутри массива...119

Использование указателей для работы со строками...120

Почему при работе со строками пользуются указателями...122

Операции с указателями других типов...122

Отличия между указателями и массивами...122

►Объявление и использование массивов указателей...124

Использование массивов строк...124

Доступ к аргументам main( )...126

Аргументы в DOS...127

Аргументы в Dev-C++...127

Аргументы в Windows...127

Глава 10. ОТЛАДКА ПРОГРАММ НА С++...128

►Определение типа ошибки...127

►Использование отладочной печати...128

Выявление "жучка" № 1...130

Выявление "жучка" № 2...131

►Использование отладчика...134

Что такое отладчик...134

Работа с отладчиком...134

Запуск тестовой программы...135

Пошаговое выполнение программы...136

Часть 3. ВВЕДЕНИЕ В КЛАССЫ...143

Глава 11. ЗНАКОМСТВО С ОБЪЕКТНО-ОРИЕНТИРОВАННЫМ ПРОГРАММИРОВАНИЕМ...145

►Микроволновые печи и уровни абстракции...145

Приготовление блюд с помощью функций...146

Приготовление "объектно-ориентированных" блюд...146

►Классификация, микроволновых печей...146

►Зачем нужна классификация...147

Глава 12. КЛАССЫ В С++...149

►Введение в классы...149

►Формат класса...149

►Обращение к членам класса...150

Глава 13. РАБОТА С КЛАССАМИ...154

►Активизация объектов...154

Моделирование реальных объектов...155

Зачем нужны функции-члены...155

►Добавление функции-члена...156

Создание функции-члена...156

Именование членов класса...157

►Вызов функций-членов...157

Обращение к функциям-членам...158

Доступ к членам из функции-члена...159

Именование текущего объекта...160

►Разрешение области видимости...161

►Определение функции-члена...162

►Определение функций-членов вне класса...164

►Перегрузка функций-членов...165

Глава 14. УКАЗАТЕЛИ НА ОБЪЕКТЫ...167

►Определение массивов и указателей...167

►Объявление массивов объектов...168

►Объявление указателей на объекты...169

Разыменование указателей на объекты...169

Использование стрелок...170

►Передача объектов функциям...171

Вызов функции с передачей объекта по значению...171

Вызов функции с передачей указателя...172

Передача объекта no ссылке...173

►Зачем, использовать указатели и ссылки...174

►Возврат к куче...175

►Сравнение указателей и ссылок...175

►Почему ссылки не используются вместо указателей...175

►Использование связанных списков...176

Другие операции над связанным списком...177

Программа LinkedListData...178

►Списки в стандартной библиотеке...180

Глава 15. ЗАЩИЩЁННЫЕ ЧЛЕНЫ КЛАССА: НЕ БЕСПОКОИТЬ!...181

►Защищённые члены...181

Зачем нужны защищённые члены...181

Как устроены защищённые члены...182

►Чем хороши защищённые члены...183

Защита внутреннего устройства класса...183

Классы с ограниченным интерфейсом...184

►Обращение к защищённым члена...184

Глава 16. СОЗДАНИЕ И УДАЛЕНИЕ ОБЪЕКТОВ...188

►Создание объектов...188

►Использование конструкторов...189

Зачем нужны конструкторы...189

Работа с конструкторами...190

Конструирование одного объекта...190

Конструирование нескольких объектов...192

Конструирование составных объектов...192

►Что такое деструктор...194

Зачем нужен деструктор...194

Работа с деструкторами...194

Глава 17. АРГУМЕНТАЦИЯ КОНСТРУИРОВАНИЯ...198

►Как снабдить конструктор аргументами...198

Зачем конструкторам нужны аргументы...198

Как использовать конструктор с аргументами...199

►Перегрузка конструктора...200

►Определение конструкторов по умолчанию...203

►Конструирование членов класса...204

Конструкторы константных членов...208

►Управление последовательностью конструирования...208

Локальные объекты создаются последовательно...209

Статические объекты создаются один раз...209

Все глобальные объекты создаются до вызова main( )...210

Порядок создания глобальных объектов не определён...210

Члены создаются в порядке их объявления...211

Деструкторы удаляют объекты в порядке, обратном порядку их создания...212

Глава 18. КОПИРУЮЩИЙ КОНСТРУКТОР...213

►Копирование объекта...213

Зачем нужен копирующий конструктор...213

Использование конструктора копирования ...214

►Автоматический конструктор копирования...215

►"Мелкие " и "глубокие " копии...217

►Временные объекты...221

Как избегать временных объектов...222

Аргумент копирующего конструктора...223

Глава 19. СТАТИЧЕСКИЕ ЧЛЕНЫ...224

►Определение статических членов...224

Зачем нужны статические члены...224

Использование статических членов...225

Обращение к статическим данным-членам...226

Применение статических данных-членов...227

►Оъявление статических функций-членов...228

►Что такое this...230

Часть 4. НАСЛЕДОВАНИЕ...231

Глава 20. НАСЛЕДОВАНИЕ КЛАССОВ...233

►Зачем, нужно наследование...234

►Как наследуется класс...234

Использование подкласса...236

Конструирование подкласса...237

Деструкция подкласса...238

►Отношение СОДЕРЖИТ...238

Глава 21. ЗНАКОМСТВО С ВИРТУАЛЬНЫМИ ФУНКЦИЯМИ-ЧЛЕНАМИ: НАСТОЯЩИЕ ЛИ ОНИ...240

►Зачем нужен полиморфизм...243

►Как работает полиморфизм...245

►Когда фунция не являтся виртуальной...246

►Виртуальные особенности...247

Глава 22. РАЗЛОЖЕНИЕ КЛАССОВ...249

►Разложение...249

►Реализация абстрактных классов...253

Концепция абстрактных классов...254

Создание полноценного класса из абстрактного...255

Передача абстрактных классов...257

Нужны ли чисто виртуальные функции...257

►Разделение исходного кода С++...259

Разделение программы - класс student...260

Определение пространства имён...261

Реализация класса student...261

Разделение программы - класс GraduateStudent...262

Реализация приложения...263

Файл проекта...264

Создание файла проекта в Dev-C++...265

Часть 5. ПОЛЕЗНЫЕ ОСОБЕННОСТИ...269

Глава 23. ОПЕРАТОР ПРИСВОЕНИЯ...271

►Сравнение операторов и функций...271

►Потоковые операторы...272

►Мелкое копирование — глубокие проблемы...272

►Переопределение оператора присвоения...273

►Защита от копирования...276

Глава 24. ИСПОЛЬЗОВАНИЕ ПОТОКОВ ВВОДА-ВЫВОДА...277

►Как работают потоки ввода-вывода...277

►Знакомство с подклассами fstream...278

►Прямое чтение из потока...282

►Что такое endl...284

►Подклассы strstream...285

►Работа с манипуляторами...287

Глава 25. ОБРАБОТКА ОШИБОК И ИСКЛЮЧЕНИЯ...290

►Зачем, нужен новый механизм обработки ошибок...291

►Механизм исключительных ситуаций...293

►Так что же мы будем бросать?...295

Глава 26. МНОЖЕСТВЕННОЕ НАСЛЕДОВАНИЕ...298

►Механизм, множественного наследования...298

►Устранение неоднозначностей множественного наследования...300

►Виртуальное наследование...301

►Конструирование объектов...306

►Отрицательные стороны множественного наследования...306

Глава 27. ШАБЛОНЫ С++...308

►Обобщение функции в шаблон...309

►Шаблоны классов...311

►Зачем нужны шаблоны классов...314

►Советы по использованию шаблонов...316

Глава 28. СТАНДАРТНАЯ БИБЛИОТЕКА ШАБЛОНОВ...317

►Контейнер string...317

►Контейнер list...320

►Итераторы...321

►Использование контейнера map...324

Часть 6.  ВЕЛИКОЛЕПНАЯ ДЕСЯТКА...329

Глава 29. ДЕСЯТЬ СПОСОБОВ ИЗБЕЖАТЬ ОШИБОК...331

►Включение всех предупреждений и сообщений об ошибках...331

►Добейтесь чистой компиляции...332

►Используйте последовательный стиль программирования...332

►Ограничивайте видимость...332

►Комментируйте свою программу...334

►Хотя бы один раз выполните программу пошагово...334

►Избегайте перегрузки операторов...334

►Работа с кучей...334

►Используйте для обработки ошибок исключительные ситуации...335

►Избегайте множественного наследования...335

Глава 30.    ДЕСЯТЬ ОСНОВНЫХ ВОЗМОЖНОСТЕЙ DEV-C++...336

►Настройка редактора по вашему вкусу...336

►Подсветка парных скобок...337

►Включение обработки исключений...337

►Включение отладочной информации...338

►Создание файла проекта...338

►Настройка справки...338

►Переустановка точек останова после редактирования файла...339

►Избегайте некорректных имён файлов...339

►Включите заголовочные файлы в ваш проект...339

►Работа с профайлером...339

Глава 31. ПРОГРАММА BUDGET...343

►BUDGET1...343

Стиль программирования...347

►BUDGET2...348

►BUDGET3...355

Реализация модуля со связанным списком...356

Работа со счетами...358

Классы связанных списков...363

Оценка бюджета...365

►BUDGET4...366

Реализация связанного списка в виде шаблона класса...366

Исходный код BUDGET4...368

Подведение итогов...373

►BUDGET5...373

Использование шаблона класса из STL...373

Создание списка счетов...378

Приложение. Содержимое прилагаемого компакт-диска

ПИКТОГРАММЫ

Технические подробности, которые можно пропустить при первом чтении.

Советы, которые помогут сохранить много времени и усилий.

Диск. Материал на прилагаемом компакт-диске.

Запомните — это важно.

Атас! Тоже важное напоминание. Это указание о том, что здесь легко допустить ошибку и даже не догадаться о ней.

Предметный указатель

ТИПОГРАФИЯ

Пожертвование и заработок в партнёрских программах

Ошибки

Примечания