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


Использовать online-читалку "Книгочей 0.2" (Не работает в Internet Explorer)


Настройки текста:


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


  Автор ФБ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, получим 12/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 простирается от -232 до +232-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 приблизительно равно 10308, т.е. 1 с 308 нулями[ 7 ].

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

[Атас!]

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

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

________

7Это не означает, будто тип double может представить 1038 разных значений; вспомните, что говорилось выше о количестве разрядов в числах этого типа. — Прим. ред.

_________________

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, а не 31/3. В отличие от математики, в компьютере число троек после точки ограничено. Умножив 3.3333 на 3, вы, вероятно, получите не 10.0, а 9.9999. Такой маленькой разницей может пренебречь человек, но не компьютер. Эта машина понимает под равенством исключительно точное равенство значений.

«Переменная типа float поддерживает точность около 6 значащих цифр, а double — 13. Я говорю "около", так как компьютер часто генерирует числа наподобие 3.3333347 из-за особенностей вычислений с плавающей точкой.»

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

В "чистой" математике количество троек после десятичной точки бесконечно, но компьютер не в состоянии работать с бесконечными числами. Поэтому при умножении 3.3333 на 3 мы получим 9.9999, а не 10, которое должно получаться при умножении 31/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 * 102 + 2 * 101 + 3 * 100


Помните, что любое число в нулевой степени равняется 1.

(обратно)

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

Использование числа 10 в качестве основания нашей системы счисления объясняется, по всей вероятности, тем, что издревле для подсчётов человек использовал пальцы рук. Учитывая особенности нашей анатомии, удобной альтернативной системой счисления можно было бы выбрать двадцатеричную[ 10 ] ( т.е. с основанием 20 ).

Если бы наша вычислительная система была заимствована у собак, то она была бы восьмеричной ( ещё один "разряд", находящийся на задней части каждой лапы, не учитывается ). Эта система счисления работала бы не менее хорошо:


12310 = 1* 82 + 7* 81 + 3* 80 = 1738


Индексы 10 и 8 указывают систему счисления: 10 — десятичная, 8 — восьмеричная. Основанием системы счисления может быть любое положительное число.

(обратно)

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

У компьютеров, видимо, пальцев поменьше ( может быть, поэтому они такие недалёкие? ). Они предпочитают пользоваться двоичной системой счисления. Число 123 переводится в двоичную систему таким образом:


12310 = 0*27+ 1*26 + 1*25 + 1*24 + 1*23 + 0*22 + 1*21 + 1*20 = 0*128 + 1*64 + 1*32 + 1*16 + 1*8 + 0*4 + 1*2 + 1*1 = 011110112


______________________

10Что и было сделано у некоторых народов, например у майя или чукчей. — Прим. ред.

_________________

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


Существует соглашение, которое гласит, что в записи двоичных чисел используются 4, 8, 16, 32 или даже 64 двоичных цифр, даже если старшие цифры — нули. Внутреннее представление числа в компьютере строится именно таким образом.

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

Поскольку основа двоичной системы счисления очень мала, для представления чисел необходимо использовать слишком большое количество битов. Для представления таких обычных чисел, как 12310, неудобно использовать выражения вида 011110112. Поэтому программисты предпочитают представлять числа блоками из четырёх битов.

С помощью одного четырёхбитового блока можно представить любое число от 0 до 15, и такая система счисления называется шестнадцатеричной ( hexadecimal ), т.е. системой по основанию 16. Часто употребляют её сокращённое название hex.

В шестнадцатеричной системе обозначения цифр от 0 до 9 остаются теми же, а числа от 10 до 15 представляются с помощью первых шести букв алфавита: А вместо 10, В вместо 11 и т.д. Следовательно, 12310 — это 7В16.


123 = 7 * 161 + В ( т.е. 11 ) * 160 = 7В16


Поскольку программисты предпочитают представлять числа с помощью 4, 8, 16 или 32 битов, шестнадцатеричные числа состоят соответственно из 1, 2, 4 или 8 шестнадцатеричных разрядов ( даже если ведущие разряды равны 0 ).

В заключение замечу, что, так как терминал не поддерживает нижний индекс, записывать шестнадцатеричные символы в виде 16 неудобно. Даже в том текстовом редакторе, который я использую сейчас, довольно неудобно всякий раз менять режимы шрифтов для ввода всего двух символов. Поэтому программисты договорились начинать шестнадцатеричные числа с ( это странное обозначение было придумано ещё во время разработки языка С ). Таким образом, 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

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

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

      ~01102 ( 0х6 )

       10012 ( 0x9 )

Таким образом получаем, что ~0x6 равно 0x9. В следующем примере продемонстрировано выполнение оператора &:

      01102

      &

      00112

      00102

Вычисляем, начиная со старших битов: 0 & 0 равно 0. В следующем бите 1 & 0 равно 0. В третьем бите 1 & 1 даёт 1, а в последнем бите 0 & 1 даёт 0.

_________________

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


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

      0x06        01102

      &             &

      0x03        00112

      0x02        00102

Расписав числа таким образом, мы получили, что 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

Копирующий конструктор так же важен, как и конструктор по умолчанию. Важен настолько, что С++ считает невозможным существование класса без копирующего конструктора. Если вы не создадите свою версию такого конструктора, С++ создаст её за вас. ( Это нес